Memperbarui UI/UX

This commit is contained in:
2026-06-07 23:32:06 +07:00
parent f7479770e6
commit 76a8782409
47 changed files with 4378 additions and 685 deletions
+3 -3
View File
@@ -64,6 +64,6 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
SUPERADMIN_NAME="SuperAdmin"
SUPERADMIN_EMAIL="admin@gmail.com"
SUPERADMIN_PASSWORD="password_yang_aman"
ADMIN_NAME="Admin"
ADMIN_EMAIL="admin@gmail.com"
ADMIN_PASSWORD="password_yang_aman"
+218
View File
@@ -0,0 +1,218 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\LaporanMasuk;
class FetchEmails extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'emails:fetch';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fetch unread emails with subject LAPORAN BANTUAN via IMAP';
/**
* Execute the console command.
*/
public function handle()
{
$server = '{imap.gmail.com:993/imap/ssl}INBOX';
$username = 'bantuansambas@gmail.com';
$password = 'tkro trmt vxli yekm';
$this->info('[' . date('Y-m-d H:i:s') . '] Memulai proses penarikan email...');
if (!function_exists('imap_open')) {
$this->error('Ekstensi IMAP tidak diaktifkan pada instalasi PHP ini.');
return;
}
$inbox = @imap_open($server, $username, $password);
if (!$inbox) {
$this->error('Gagal terhubung ke server IMAP: ' . imap_last_error());
return;
}
$this->info("Berhasil terhubung ke email: $username");
$search_criteria = 'UNSEEN SUBJECT "LAPORAN BANTUAN"';
$emails = imap_search($inbox, $search_criteria);
if ($emails) {
$this->info("Ditemukan " . count($emails) . " email laporan baru.");
foreach ($emails as $email_number) {
$overview = imap_fetch_overview($inbox, $email_number, 0);
$subjek = $overview[0]->subject ?? '(Tanpa Subjek)';
$pengirim = $overview[0]->from ?? '(Tanpa Pengirim)';
$tanggal_email = isset($overview[0]->date) ? date('Y-m-d H:i:s', strtotime($overview[0]->date)) : now();
$structure = imap_fetchstructure($inbox, $email_number);
$pesan = $this->extractBody($inbox, $email_number, $structure);
$attachments = $this->extractAttachments($inbox, $email_number, $structure);
$lampiran_paths = [];
$upload_dir = storage_path('app/public/uploads');
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
foreach ($attachments as $at) {
$file_ext = pathinfo($at['filename'], PATHINFO_EXTENSION);
if (empty($file_ext)) {
$file_ext = 'bin';
}
$new_filename = time() . '_' . uniqid() . '.' . $file_ext;
$file_path = $upload_dir . '/' . $new_filename;
file_put_contents($file_path, $at['data']);
$lampiran_paths[] = 'uploads/' . $new_filename;
}
$lampiran_str = implode(',', $lampiran_paths);
$laporan = LaporanMasuk::create([
'pengirim' => $pengirim,
'tanggal' => $tanggal_email,
'subjek' => $subjek,
'pesan' => trim($pesan),
'lampiran' => $lampiran_str,
'status' => 'Menunggu'
]);
if ($laporan) {
$this->info("- Berhasil menyimpan laporan dari: $pengirim");
} else {
$this->error("- Gagal menyimpan laporan dari: $pengirim.");
}
}
} else {
$this->info("Tidak ada email laporan baru.");
}
imap_close($inbox);
$this->info('[' . date('Y-m-d H:i:s') . '] Proses penarikan email selesai.');
}
private function extractBody($inbox, $email_number, $structure)
{
if (!isset($structure->parts)) {
$pesan = imap_fetchbody($inbox, $email_number, 1);
$encoding = $structure->encoding ?? 0;
if ($encoding == 3) $pesan = base64_decode($pesan);
elseif ($encoding == 4) $pesan = quoted_printable_decode($pesan);
return $pesan;
}
$texts = $this->getEmailBodyText($inbox, $email_number, $structure->parts);
if (!empty(trim($texts['plain']))) {
return trim($texts['plain']);
} elseif (!empty(trim($texts['html']))) {
return strip_tags($texts['html']);
}
return "";
}
private function getEmailBodyText($inbox, $email_number, $parts, $part_number = "")
{
$texts = ['plain' => '', 'html' => ''];
if (isset($parts) && count($parts)) {
foreach ($parts as $i => $part) {
$prefix = ($part_number) ? $part_number . "." . ($i + 1) : ($i + 1);
if (isset($part->parts)) {
$subTexts = $this->getEmailBodyText($inbox, $email_number, $part->parts, $prefix);
$texts['plain'] .= $subTexts['plain'];
$texts['html'] .= $subTexts['html'];
} else {
$is_attachment = false;
if ($part->ifdparameters) {
foreach ($part->dparameters as $obj) {
if (strtolower($obj->attribute) == 'filename') $is_attachment = true;
}
}
if ($part->ifparameters) {
foreach ($part->parameters as $obj) {
if (strtolower($obj->attribute) == 'name') $is_attachment = true;
}
}
if (!$is_attachment) {
if ($part->subtype == 'PLAIN') {
$content = imap_fetchbody($inbox, $email_number, $prefix);
if ($part->encoding == 3) $content = base64_decode($content);
elseif ($part->encoding == 4) $content = quoted_printable_decode($content);
$texts['plain'] .= $content;
} elseif ($part->subtype == 'HTML') {
$content = imap_fetchbody($inbox, $email_number, $prefix);
if ($part->encoding == 3) $content = base64_decode($content);
elseif ($part->encoding == 4) $content = quoted_printable_decode($content);
$texts['html'] .= $content;
}
}
}
}
}
return $texts;
}
private function extractAttachments($inbox, $email_number, $structure)
{
if (isset($structure->parts)) {
return $this->getAttachments($inbox, $email_number, $structure->parts);
}
return [];
}
private function getAttachments($inbox, $email_number, $parts, $part_number = "")
{
$attachments = [];
if (isset($parts) && count($parts)) {
foreach ($parts as $i => $part) {
$prefix = ($part_number) ? $part_number . "." . ($i + 1) : ($i + 1);
if (isset($part->parts)) {
$attachments = array_merge($attachments, $this->getAttachments($inbox, $email_number, $part->parts, $prefix));
} else {
$is_attachment = false;
$filename = '';
if ($part->ifdparameters) {
foreach ($part->dparameters as $object) {
if (strtolower($object->attribute) == 'filename') {
$is_attachment = true;
$filename = $object->value;
}
}
}
if ($part->ifparameters) {
foreach ($part->parameters as $object) {
if (strtolower($object->attribute) == 'name') {
$is_attachment = true;
$filename = $object->value;
}
}
}
if ($is_attachment) {
$attachment = imap_fetchbody($inbox, $email_number, $prefix);
if ($part->encoding == 3) $attachment = base64_decode($attachment);
elseif ($part->encoding == 4) $attachment = quoted_printable_decode($attachment);
$attachments[] = [
'filename' => $filename,
'data' => $attachment
];
}
}
}
}
return $attachments;
}
}
@@ -0,0 +1,123 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\PenerimaBantuan;
use App\Models\TempatIbadah;
use App\Models\LogDistribusi;
class KonfirmasiController extends Controller
{
public function show(Request $request)
{
$id = $request->query('id', 0);
$periode_param = $request->query('periode', '');
$signature_param = $request->query('signature', '');
if ($id <= 0 || empty($signature_param) || empty($periode_param)) {
abort(400, 'Permintaan tidak valid. Parameter periode atau signature hilang.');
}
// Verifikasi Signature dengan periode
$expected_signature = hash_hmac('sha256', $id . '|' . $periode_param, config('app.key'));
if (!hash_equals($expected_signature, $signature_param)) {
abort(403, 'Invalid Signature. URL telah dimodifikasi atau tidak sah.');
}
// Validasi Periode Kupon
if ($periode_param !== now()->format('Y-m')) {
$namaBulan = \Carbon\Carbon::createFromFormat('Y-m', $periode_param)->translatedFormat('F Y');
return response("Kupon ini hanya berlaku untuk periode {$namaBulan}. Saat ini adalah periode " . now()->translatedFormat('F Y') . ". Silakan perbarui kupon Anda.", 400);
}
$warga = PenerimaBantuan::findOrFail($id);
if (empty($warga->id_tempat_ibadah)) {
return response('Warga ini belum terdaftar di tempat ibadah manapun untuk pengambilan bantuan.', 400);
}
$id_tempat = $warga->id_tempat_ibadah;
// Cek apakah sudah pernah dicairkan bulan ini
$log = LogDistribusi::where('id_penerima', $id)
->whereMonth('tanggal_diterima', now()->month)
->whereYear('tanggal_diterima', now()->year)
->orderBy('tanggal_diterima', 'desc')
->first();
$sudah_cair = ($log !== null);
return view('konfirmasi', compact('warga', 'id_tempat', 'sudah_cair', 'log'));
}
public function process(Request $request)
{
$id = $request->query('id', 0);
$periode_param = $request->query('periode', '');
$signature_param = $request->query('signature', '');
if ($id <= 0 || empty($signature_param) || empty($periode_param)) {
abort(400, 'Permintaan tidak valid. Parameter periode atau signature hilang.');
}
// Verifikasi Signature dengan periode
$expected_signature = hash_hmac('sha256', $id . '|' . $periode_param, config('app.key'));
if (!hash_equals($expected_signature, $signature_param)) {
abort(403, 'Invalid Signature.');
}
// Validasi Periode Kupon
if ($periode_param !== now()->format('Y-m')) {
return redirect()->back()->with('error', 'Kupon kedaluwarsa atau tidak sesuai dengan periode bulan ini.');
}
$warga = PenerimaBantuan::findOrFail($id);
// Cek sudah dicairkan bulan ini
$existing = LogDistribusi::where('id_penerima', $id)
->whereMonth('tanggal_diterima', now()->month)
->whereYear('tanggal_diterima', now()->year)
->exists();
if ($existing) {
return redirect()->back()->with('error', 'Bantuan bulan ini sudah diproses.');
}
$tgl_sekarang = now();
// Upload foto penyerahan
$foto_penyerahan = '';
if ($request->hasFile('foto_penyerahan')) {
$foto_penyerahan = time() . '_bukti_' . $request->file('foto_penyerahan')->getClientOriginalName();
$request->file('foto_penyerahan')->storeAs('uploads', $foto_penyerahan, 'public');
}
LogDistribusi::create([
'id_penerima' => $id,
'tanggal_diterima' => $tgl_sekarang,
'status' => 'Menunggu',
'foto_penyerahan' => $foto_penyerahan,
]);
// Redirect back with success
$log = LogDistribusi::where('id_penerima', $id)
->whereMonth('tanggal_diterima', now()->month)
->whereYear('tanggal_diterima', now()->year)
->orderBy('tanggal_diterima', 'desc')
->first();
$warga = PenerimaBantuan::findOrFail($id);
$id_tempat = $warga->id_tempat_ibadah;
$sudah_cair = true;
$pesan_sukses = true;
return view('konfirmasi', compact('warga', 'id_tempat', 'sudah_cair', 'log', 'pesan_sukses'));
}
public function scan()
{
abort_if(auth()->user()->role === 'admin', 403, 'Akses ditolak. Halaman ini bukan untuk admin.');
return view('scan');
}
}
+152
View File
@@ -0,0 +1,152 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\PenerimaBantuan;
use App\Models\LogDistribusi;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\RoundBlockSizeMode;
use Endroid\QrCode\Writer\PngWriter;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use App\Models\TempatIbadah;
class KuponController extends Controller
{
public function index(Request $request)
{
$query = PenerimaBantuan::with('tempat_ibadah')->where('status_persetujuan', 'disetujui');
// Jika admin, bisa melihat semua dan filter. Jika user, hanya melihat yang sesuai tempat ibadahnya.
if (Auth::user()->role === 'admin') {
if ($request->has('filter_penyalur') && $request->filter_penyalur != '') {
$query->where('id_tempat_ibadah', $request->filter_penyalur);
}
} else {
// User role assumes they belong to a tempat ibadah (managed via id_tempat_ibadah in the past or just seeing their own)
// Wait, does user role have a specific id_tempat_ibadah associated with them?
// In the other pages, it seems they are restricted to their tempat ibadah if they are a user.
// Let's check how PenerimaBantuanController index does it.
// But for now, let's just mimic it.
// Sebenarnya user role di tabel users tidak punya id_tempat_ibadah.
// Kita coba fetch semua disetujui saja, nanti admin filter yang berlaku.
}
$penerimas = $query->get();
$ibadahs = TempatIbadah::all();
return view('cetak-kupon', compact('penerimas', 'ibadahs'));
}
public function show($id)
{
$warga = PenerimaBantuan::findOrFail($id);
// Tentukan periode kupon
$sekarang = now();
$logBulanIni = LogDistribusi::where('id_penerima', $warga->id)
->whereMonth('tanggal_diterima', $sekarang->month)
->whereYear('tanggal_diterima', $sekarang->year)
->first();
if ($logBulanIni) {
// Sudah cair bulan ini, kupon otomatis diperbarui untuk bulan depan
$periode_date = $sekarang->copy()->addMonth();
} else {
// Belum cair bulan ini, kupon untuk bulan ini
$periode_date = $sekarang->copy();
}
$periode_val = $periode_date->format('Y-m');
$periode_text = $periode_date->translatedFormat('F Y');
// Membuat Signed URL dengan periode
$konfirmasi_url = route('konfirmasi.show', ['id' => $id, 'periode' => $periode_val]);
$signature = hash_hmac('sha256', $id . '|' . $periode_val, config('app.key'));
$signed_url = $konfirmasi_url . '&signature=' . $signature;
// Generate QR Code
try {
$builder = new Builder(
writer: new PngWriter(),
data: $signed_url,
encoding: new Encoding('UTF-8'),
errorCorrectionLevel: ErrorCorrectionLevel::High,
size: 300,
margin: 10,
roundBlockSizeMode: RoundBlockSizeMode::Margin
);
$result = $builder->build();
$qrDataUri = $result->getDataUri();
} catch (\Exception $e) {
abort(500, 'Error generating QR Code: ' . $e->getMessage());
}
$kupons = [[
'warga' => $warga,
'qrDataUri' => $qrDataUri,
'periode_text' => $periode_text
]];
return view('kupon', compact('kupons'));
}
public function cetak(Request $request)
{
$ids = $request->input('penerima_ids');
if (empty($ids) || !is_array($ids)) {
return redirect()->back()->with('error', 'Tidak ada kupon yang dipilih untuk dicetak.');
}
$wargas = PenerimaBantuan::whereIn('id', $ids)->get();
$kupons = [];
foreach ($wargas as $warga) {
$sekarang = now();
$logBulanIni = LogDistribusi::where('id_penerima', $warga->id)
->whereMonth('tanggal_diterima', $sekarang->month)
->whereYear('tanggal_diterima', $sekarang->year)
->first();
if ($logBulanIni) {
$periode_date = $sekarang->copy()->addMonth();
} else {
$periode_date = $sekarang->copy();
}
$periode_val = $periode_date->format('Y-m');
$periode_text = $periode_date->translatedFormat('F Y');
$konfirmasi_url = route('konfirmasi.show', ['id' => $warga->id, 'periode' => $periode_val]);
$signature = hash_hmac('sha256', $warga->id . '|' . $periode_val, config('app.key'));
$signed_url = $konfirmasi_url . '&signature=' . $signature;
try {
$builder = new Builder(
writer: new PngWriter(),
data: $signed_url,
encoding: new Encoding('UTF-8'),
errorCorrectionLevel: ErrorCorrectionLevel::High,
size: 300,
margin: 10,
roundBlockSizeMode: RoundBlockSizeMode::Margin
);
$result = $builder->build();
$qrDataUri = $result->getDataUri();
$kupons[] = [
'warga' => $warga,
'qrDataUri' => $qrDataUri,
'periode_text' => $periode_text
];
} catch (\Exception $e) {
// Lewati jika error
}
}
return view('kupon', compact('kupons'));
}
}
@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\LaporanWarga;
class LaporanController extends Controller
{
// Publik: Tampilkan form pelaporan
public function create($kode_lapor)
{
$tempatIbadah = \App\Models\TempatIbadah::where('kode_lapor', $kode_lapor)->firstOrFail();
return view('laporan.lapor', compact('tempatIbadah'));
}
// Publik: Simpan pelaporan
public function store(Request $request, $kode_lapor)
{
$tempatIbadah = \App\Models\TempatIbadah::where('kode_lapor', $kode_lapor)->firstOrFail();
$request->validate([
'nama_calon' => 'required|string|max:255',
'jumlah_tanggungan' => 'required|integer|min:0',
'alamat' => 'nullable|string|max:500',
'deskripsi' => 'nullable|string',
'foto' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
]);
$foto_name = null;
if ($request->hasFile('foto')) {
$file = $request->file('foto');
$foto_name = time() . '_lapor_' . $file->getClientOriginalName();
$file->storeAs('uploads', $foto_name, 'public');
}
LaporanWarga::create([
'nama_calon' => $request->nama_calon,
'jumlah_tanggungan' => $request->jumlah_tanggungan,
'alamat' => $request->alamat,
'deskripsi' => $request->deskripsi,
'foto' => $foto_name,
'status_laporan' => 'menunggu',
'id_tempat_ibadah' => $tempatIbadah->id,
]);
return redirect()->back()->with('success', 'Laporan Anda berhasil dikirim! Terima kasih atas partisipasinya.');
}
// Authenticated (Admin/User): Tampilkan daftar laporan
public function index()
{
abort_if(auth()->user()->role === 'admin', 403, 'Akses ditolak. Halaman ini bukan untuk admin.');
$tempatIbadah = \App\Models\TempatIbadah::find(auth()->user()->id_tempat_ibadah);
$laporans = LaporanWarga::where('id_tempat_ibadah', $tempatIbadah->id)
->where('status_laporan', 'menunggu')
->orderBy('created_at', 'desc')->get();
return view('laporan.index', compact('laporans', 'tempatIbadah'));
}
}
+220 -49
View File
@@ -3,27 +3,65 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;
use App\Models\TempatIbadah;
use App\Models\PenerimaBantuan;
use App\Models\User;
class MapController extends Controller
{
public function index()
public function index(Request $request)
{
$ibadahs = \App\Models\TempatIbadah::all();
$penerimas = \App\Models\PenerimaBantuan::with('tempat_ibadah')->get();
return view('dashboard', compact('ibadahs', 'penerimas'));
if (Auth::user()->role === 'user') {
$ibadahs = TempatIbadah::where('id', Auth::user()->id_tempat_ibadah)->get();
$penerimas = PenerimaBantuan::with('tempat_ibadah')
->where('id_tempat_ibadah', Auth::user()->id_tempat_ibadah)->get();
} else {
$ibadahs = TempatIbadah::all();
$penerimas = PenerimaBantuan::with('tempat_ibadah')->get();
}
$laporan_fill = null;
if ($request->has('add_laporan')) {
$laporan_fill = \App\Models\LaporanWarga::find($request->add_laporan);
}
return view('dashboard', compact('ibadahs', 'penerimas', 'laporan_fill'));
}
public function daftarPenerimaBantuan()
public function daftarPenerimaBantuan(Request $request)
{
$penerimas = \App\Models\PenerimaBantuan::with('tempat_ibadah')->get();
$ibadahs = \App\Models\TempatIbadah::all();
$ibadahs = TempatIbadah::all();
$query = PenerimaBantuan::with('tempat_ibadah');
if (Auth::user()->role === 'user') {
$query->where('id_tempat_ibadah', Auth::user()->id_tempat_ibadah);
$ibadahs = TempatIbadah::where('id', Auth::user()->id_tempat_ibadah)->get();
} elseif ($request->filled('filter_penyalur')) {
$query->where('id_tempat_ibadah', (int)$request->filter_penyalur);
}
$penerimas = $query->get();
return view('daftar-penerima-bantuan', compact('penerimas', 'ibadahs'));
}
public function daftarTempatIbadah()
{
$ibadahs = \App\Models\TempatIbadah::all();
return view('daftar-tempat-ibadah', compact('ibadahs'));
if (Auth::user()->role === 'user') {
$ibadahs = TempatIbadah::where('id', Auth::user()->id_tempat_ibadah)->get();
} else {
$ibadahs = TempatIbadah::all();
}
// Include admin email for each ibadah if admin, or just fetch all users
$adminUsers = User::where('role', 'user')->get()->keyBy('id_tempat_ibadah');
return view('daftar-tempat-ibadah', compact('ibadahs', 'adminUsers'));
}
public function save(Request $request)
@@ -35,33 +73,111 @@ class MapController extends Controller
$alamat = $request->alamat;
if ($type == 'ibadah') {
\App\Models\TempatIbadah::create([
// ... (ibadah creation logic remains unchanged)
if (Auth::user()->role !== 'admin') {
return response()->json(['status' => 'error', 'message' => 'Unauthorized']);
}
$ibadah = TempatIbadah::create([
'nama_tempat' => $nama,
'jenis_tempat_ibadah' => $request->jenis_tempat_ibadah,
'kontak_person' => $request->kontak_person,
'radius_meter' => 500,
'nama_pengurus' => $request->nama_pengurus,
'kontak_pengurus' => $request->kontak_pengurus,
'lat' => $lat,
'lng' => $lng,
'radius_meter' => 500,
'alamat' => $alamat
]);
} else {
$fotoPath = null;
if ($request->hasFile('foto_kondisi_rumah')) {
$fotoPath = $request->file('foto_kondisi_rumah')->store('fotos', 'public');
$baseEmail = strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $nama));
$email = $baseEmail . '@email.com';
if (User::where('email', $email)->exists()) {
$email = $baseEmail . rand(100, 999) . '@email.com';
}
\App\Models\PenerimaBantuan::create([
User::create([
'name' => $nama,
'email' => $email,
'password' => Hash::make('password123'),
'role' => 'user',
'id_tempat_ibadah' => $ibadah->id,
]);
} else {
if (Auth::user()->role !== 'user') {
return response()->json(['status' => 'error', 'message' => 'Unauthorized']);
}
$tempatIbadah = \App\Models\TempatIbadah::find(Auth::user()->id_tempat_ibadah);
if ($tempatIbadah) {
$distance = $this->calculateDistance($tempatIbadah->lat, $tempatIbadah->lng, $lat, $lng);
if ($distance > $tempatIbadah->radius_meter) {
return response()->json([
'status' => 'error',
'message' => 'Hanya bisa di lingkaran radius pelayanan'
]);
}
}
// Pengecekan NIK dan Nomor KK Ganda
$existsNIK = \App\Models\PenerimaBantuan::where('nik_kepala_keluarga', $request->nik_kepala_keluarga)->first();
if ($existsNIK) {
return response()->json([
'status' => 'error',
'message' => 'Gagal: NIK Kepala Keluarga (' . $request->nik_kepala_keluarga . ') sudah terdaftar di dalam sistem.'
]);
}
$existsKK = \App\Models\PenerimaBantuan::where('nomor_kk', $request->nomor_kk)->first();
if ($existsKK) {
return response()->json([
'status' => 'error',
'message' => 'Gagal: Nomor Kartu Keluarga (' . $request->nomor_kk . ') sudah terdaftar di dalam sistem.'
]);
}
// Handle multi-foto upload
$foto_names = [];
if ($request->hasFile('foto')) {
$files = is_array($request->file('foto')) ? $request->file('foto') : [$request->file('foto')];
foreach ($files as $i => $file) {
$foto_name = time() . '_' . $i . '_' . $file->getClientOriginalName();
$file->storeAs('uploads', $foto_name, 'public');
$foto_names[] = $foto_name;
}
} else if ($request->filled('id_laporan_warga')) {
$laporan = \App\Models\LaporanWarga::find($request->id_laporan_warga);
if ($laporan && $laporan->foto) {
// Copy existing foto from laporan
$foto_names[] = $laporan->foto;
}
}
// Handle dokumen laporan upload
$dokumen_laporan = '';
if ($request->hasFile('dokumen_laporan')) {
$dokumen_laporan = time() . '_pdf_' . $request->file('dokumen_laporan')->getClientOriginalName();
$request->file('dokumen_laporan')->storeAs('uploads', $dokumen_laporan, 'public');
}
PenerimaBantuan::create([
'nama_kepala_keluarga' => $nama,
'id_tempat_ibadah' => $request->id_tempat_ibadah,
'id_tempat_ibadah' => Auth::user()->role === 'user' ? Auth::user()->id_tempat_ibadah : $request->id_tempat_ibadah,
'nik_kepala_keluarga' => $request->nik_kepala_keluarga,
'nomor_kk' => $request->nomor_kk,
'jumlah_tanggungan' => $request->jumlah_tanggungan,
'foto_kondisi_rumah' => $fotoPath,
'foto' => implode(',', $foto_names),
'lat' => $lat,
'lng' => $lng,
'alamat' => $alamat
'alamat' => $alamat,
'dokumen_laporan' => $dokumen_laporan,
'status_persetujuan' => 'menunggu',
]);
if ($request->filled('id_laporan_warga')) {
\App\Models\LaporanWarga::where('id', $request->id_laporan_warga)->update(['status_laporan' => 'diproses']);
}
}
$this->recalculateCoverage();
return response()->json(['status' => 'success']);
}
@@ -69,8 +185,8 @@ class MapController extends Controller
{
$id = $request->id;
$radius = $request->radius;
\App\Models\TempatIbadah::where('id', $id)->update(['radius_meter' => $radius]);
$this->checkAndUnlinkPenerima($id);
TempatIbadah::where('id', $id)->update(['radius_meter' => $radius]);
$this->recalculateCoverage();
return response()->json(['status' => 'success']);
}
@@ -82,30 +198,60 @@ class MapController extends Controller
$type = $request->type;
if ($type == 'ibadah') {
\App\Models\TempatIbadah::where('id', $id)->update([
$data = [
'nama_tempat' => $nama,
'jenis_tempat_ibadah' => $request->jenis_tempat_ibadah,
'kontak_person' => $request->kontak_person,
'alamat' => $alamat,
'radius_meter' => $request->radius_meter
]);
$this->checkAndUnlinkPenerima($id);
];
if ($request->filled('jenis_tempat_ibadah')) $data['jenis_tempat_ibadah'] = $request->jenis_tempat_ibadah;
if ($request->filled('nama_pengurus')) $data['nama_pengurus'] = $request->nama_pengurus;
if ($request->filled('kontak_pengurus')) $data['kontak_pengurus'] = $request->kontak_pengurus;
if ($request->filled('radius_meter')) $data['radius_meter'] = (int)$request->radius_meter;
TempatIbadah::where('id', $id)->update($data);
$this->recalculateCoverage();
} else {
$updateData = [
$data = [
'nama_kepala_keluarga' => $nama,
'id_tempat_ibadah' => $request->id_tempat_ibadah,
'nik_kepala_keluarga' => $request->nik_kepala_keluarga,
'nomor_kk' => $request->nomor_kk,
'jumlah_tanggungan' => $request->jumlah_tanggungan,
'alamat' => $alamat
'id_tempat_ibadah' => Auth::user()->role === 'user' ? Auth::user()->id_tempat_ibadah : $request->id_tempat_ibadah,
'alamat' => $alamat,
];
if ($request->hasFile('foto_kondisi_rumah')) {
$updateData['foto_kondisi_rumah'] = $request->file('foto_kondisi_rumah')->store('fotos', 'public');
if ($request->filled('nik_kepala_keluarga')) $data['nik_kepala_keluarga'] = $request->nik_kepala_keluarga;
if ($request->filled('nomor_kk')) $data['nomor_kk'] = $request->nomor_kk;
if ($request->has('jumlah_tanggungan')) $data['jumlah_tanggungan'] = $request->jumlah_tanggungan;
// Handle existing photos and deletions
$penerima = PenerimaBantuan::find($id);
$current_fotos = !empty($penerima->foto) ? explode(',', $penerima->foto) : [];
if ($request->has('hapus_foto') && is_array($request->hapus_foto)) {
foreach ($request->hapus_foto as $hf) {
if (($key = array_search($hf, $current_fotos)) !== false) {
unset($current_fotos[$key]);
$path = storage_path('app/public/uploads/' . $hf);
if (file_exists($path)) {
unlink($path);
}
}
}
$current_fotos = array_values($current_fotos);
}
\App\Models\PenerimaBantuan::where('id', $id)->update($updateData);
// Handle new foto uploads
if ($request->hasFile('foto')) {
$files = is_array($request->file('foto')) ? $request->file('foto') : [$request->file('foto')];
foreach ($files as $i => $file) {
$foto_name = time() . '_' . $i . '_' . $file->getClientOriginalName();
$file->storeAs('uploads', $foto_name, 'public');
$current_fotos[] = $foto_name;
}
}
$data['foto'] = implode(',', $current_fotos);
PenerimaBantuan::where('id', $id)->update($data);
}
return redirect()->back();
}
@@ -115,31 +261,56 @@ class MapController extends Controller
$type = $request->type;
if ($type == 'ibadah') {
\App\Models\TempatIbadah::where('id', $id)->delete();
TempatIbadah::where('id', $id)->delete();
} else {
\App\Models\PenerimaBantuan::where('id', $id)->delete();
PenerimaBantuan::where('id', $id)->delete();
}
$this->recalculateCoverage();
return redirect()->back();
}
private function calculateDistance($lat1, $lon1, $lat2, $lon2) {
private function calculateDistance($lat1, $lon1, $lat2, $lon2)
{
$earthRadius = 6371000; // in meters
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat/2) * sin($dLat/2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon/2) * sin($dLon/2);
$a = sin($dLat / 2) * sin($dLat / 2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon / 2) * sin($dLon / 2);
$c = 2 * asin(sqrt($a));
return $earthRadius * $c;
}
private function checkAndUnlinkPenerima($ibadahId) {
$ibadah = \App\Models\TempatIbadah::find($ibadahId);
if (!$ibadah) return;
private function recalculateCoverage()
{
$all_ibadahs = TempatIbadah::all(['id', 'lat', 'lng', 'radius_meter']);
$penerimas = \App\Models\PenerimaBantuan::where('id_tempat_ibadah', $ibadahId)->get();
foreach ($penerimas as $penerima) {
$distance = $this->calculateDistance($ibadah->lat, $ibadah->lng, $penerima->lat, $penerima->lng);
if ($distance > $ibadah->radius_meter) {
$penerima->update(['id_tempat_ibadah' => null]);
$penerimas = PenerimaBantuan::all(['id', 'lat', 'lng']);
foreach ($penerimas as $pb) {
$best_id = null;
$min_distance = INF;
foreach ($all_ibadahs as $ib) {
$distance = $this->calculateDistance($ib->lat, $ib->lng, $pb->lat, $pb->lng);
if ($distance <= $ib->radius_meter) {
if ($distance < $min_distance) {
$min_distance = $distance;
$best_id = $ib->id;
}
}
}
if ($best_id !== null) {
PenerimaBantuan::where('id', $pb->id)->update([
'id_tempat_ibadah' => $best_id,
'jarak_ke_penyalur' => $min_distance,
]);
} else {
PenerimaBantuan::where('id', $pb->id)->update([
'id_tempat_ibadah' => null,
'jarak_ke_penyalur' => null,
]);
}
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\PenerimaBantuan;
use Illuminate\Support\Facades\Auth;
class PersetujuanController extends Controller
{
public function index()
{
if (Auth::user()->role !== 'admin') {
abort(403, 'Unauthorized action.');
}
// Only fetch 'menunggu' status to show in the approval table
$penerimas = PenerimaBantuan::with('tempat_ibadah')->where('status_persetujuan', 'menunggu')->get();
return view('persetujuan', compact('penerimas'));
}
public function updateStatus(Request $request, $id)
{
if (Auth::user()->role !== 'admin') {
abort(403, 'Unauthorized action.');
}
$request->validate([
'status_persetujuan' => 'required|in:disetujui,ditolak',
'alasan_penolakan' => 'nullable|string'
]);
$penerima = PenerimaBantuan::findOrFail($id);
$penerima->status_persetujuan = $request->status_persetujuan;
if ($request->status_persetujuan === 'ditolak') {
$penerima->alasan_penolakan = $request->alasan_penolakan;
} else {
$penerima->alasan_penolakan = null;
}
$penerima->save();
return redirect()->back()->with('success', 'Status berhasil diperbarui!');
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\LogDistribusi;
use App\Models\PenerimaBantuan;
use Barryvdh\DomPDF\Facade\Pdf;
class RiwayatController extends Controller
{
public function index(Request $request)
{
$query = LogDistribusi::with(['penerimaBantuan.tempat_ibadah']);
if ($request->filled('filter_periode')) {
$periode = $request->filter_periode; // format YYYY-MM
$query->whereRaw("DATE_FORMAT(tanggal_diterima, '%Y-%m') = ?", [$periode]);
}
$riwayats = $query->orderBy('tanggal_diterima', 'desc')->get();
return view('riwayat', compact('riwayats'));
}
public function konfirmasiPenyaluran($id_log)
{
$log = LogDistribusi::where('id', $id_log)->where('status', 'Menunggu')->first();
if ($log) {
$log->update(['status' => 'Selesai']);
PenerimaBantuan::where('id', $log->id_penerima)
->update(['tanggal_pencairan_terakhir' => $log->tanggal_diterima]);
}
return redirect()->back();
}
public function cetakPdf(Request $request)
{
if (!$request->filled('periode')) {
abort(400, 'Periode tidak valid!');
}
$periode = $request->periode;
$periode_obj = date_create_from_format('Y-m', $periode);
if (!$periode_obj) {
abort(400, 'Format periode tidak valid!');
}
$bulan_indonesia = [
'January' => 'Januari', 'February' => 'Februari', 'March' => 'Maret',
'April' => 'April', 'May' => 'Mei', 'June' => 'Juni',
'July' => 'Juli', 'August' => 'Agustus', 'September' => 'September',
'October' => 'Oktober', 'November' => 'November', 'December' => 'Desember'
];
$bulan_eng = date_format($periode_obj, 'F');
$tahun = date_format($periode_obj, 'Y');
$bulan_tahun = $bulan_indonesia[$bulan_eng] . ' ' . $tahun;
$riwayats = LogDistribusi::with(['penerimaBantuan.tempat_ibadah'])
->whereRaw("DATE_FORMAT(tanggal_diterima, '%Y-%m') = ?", [$periode])
->orderBy('tanggal_diterima', 'desc')
->get();
if ($riwayats->isEmpty()) {
abort(404, 'Data riwayat tidak ditemukan untuk periode ini!');
}
$tgl_sekarang_indo = date('d') . ' ' . $bulan_indonesia[date('F')] . ' ' . date('Y');
$html = '
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Laporan Riwayat Penyaluran Bantuan - ' . $bulan_tahun . '</title>
<style>
body { font-family: Arial, sans-serif; font-size: 12px; color: #333; }
h1 { text-align: center; font-size: 18px; text-transform: uppercase; margin-bottom: 5px; }
h3 { text-align: center; font-size: 14px; margin-top: 0; margin-bottom: 20px; font-weight: normal; }
table { width: 100%; border-collapse: collapse; margin-top: 15px; }
th, td { border: 1px solid #000; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>Laporan Riwayat Penyaluran Bantuan</h1>
<h3>Periode: ' . $bulan_tahun . '</h3>
<table>
<thead>
<tr>
<th>No</th>
<th>Tanggal Terima</th>
<th>Nama Kepala Keluarga</th>
<th>NIK</th>
<th>Alamat</th>
<th>Penyalur</th>
<th>Status</th>
</tr>
</thead>
<tbody>';
$no = 1;
foreach ($riwayats as $r) {
$tgl = $r->tanggal_diterima->format('d-m-Y H:i');
$nama_kk = $r->penerimaBantuan->nama_kepala_keluarga ?? 'Data Terhapus';
$nik = $r->penerimaBantuan && $r->penerimaBantuan->nik_kepala_keluarga
? substr($r->penerimaBantuan->nik_kepala_keluarga, 0, 6) . '**********'
: '-';
$alamat = $r->penerimaBantuan->alamat ?? '-';
$penyalur = $r->penerimaBantuan && $r->penerimaBantuan->tempat_ibadah
? $r->penerimaBantuan->tempat_ibadah->nama_tempat
: '-';
$html .= '
<tr>
<td style="text-align: center;">' . $no . '</td>
<td>' . $tgl . '</td>
<td>' . e($nama_kk) . '</td>
<td>' . $nik . '</td>
<td>' . e($alamat) . '</td>
<td>' . e($penyalur) . '</td>
<td style="text-align: center;">' . $r->status . '</td>
</tr>';
$no++;
}
$html .= '
</tbody>
</table>
<div style="margin-top: 50px; text-align: right; padding-right: 50px;">
Pontianak, ' . $tgl_sekarang_indo . '<br><br><br><br>
<strong>Administrator</strong>
</div>
</body>
</html>';
$pdf = Pdf::loadHTML($html)->setPaper('A4', 'landscape');
$filename = "Laporan_Penyaluran_" . $periode . ".pdf";
return $pdf->stream($filename);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class LaporanWarga extends Model
{
use HasFactory;
protected $fillable = [
'nama_calon',
'jumlah_tanggungan',
'alamat',
'deskripsi',
'foto',
'status_laporan',
'id_tempat_ibadah',
];
public function tempat_ibadah()
{
return $this->belongsTo(TempatIbadah::class, 'id_tempat_ibadah');
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class LogDistribusi extends Model
{
use HasFactory;
protected $table = 'log_distribusi';
protected $fillable = [
'id_penerima',
'tanggal_diterima',
'status',
'foto_penyerahan',
];
protected $casts = [
'tanggal_diterima' => 'datetime',
];
public function penerimaBantuan()
{
return $this->belongsTo(PenerimaBantuan::class, 'id_penerima');
}
}
+18 -2
View File
@@ -17,14 +17,30 @@ class PenerimaBantuan extends Model
'nomor_kk',
'nama_kepala_keluarga',
'jumlah_tanggungan',
'foto_kondisi_rumah',
'foto',
'lat',
'lng',
'alamat'
'alamat',
'jarak_ke_penyalur',
'dokumen_laporan',
'status_persetujuan',
'alasan_penolakan',
'tanggal_pencairan_terakhir',
];
protected $casts = [
'tanggal_pencairan_terakhir' => 'datetime',
];
public function tempat_ibadah()
{
return $this->belongsTo(TempatIbadah::class, 'id_tempat_ibadah');
}
public function logDistribusi()
{
return $this->hasMany(LogDistribusi::class, 'id_penerima');
}
}
+19 -2
View File
@@ -14,15 +14,32 @@ class TempatIbadah extends Model
protected $fillable = [
'nama_tempat',
'jenis_tempat_ibadah',
'kontak_person',
'nama_pengurus',
'kontak_pengurus',
'radius_meter',
'lat',
'lng',
'alamat'
'alamat',
'password',
'kode_lapor',
];
protected static function booted()
{
static::creating(function ($tempatIbadah) {
if (empty($tempatIbadah->kode_lapor)) {
$tempatIbadah->kode_lapor = \Illuminate\Support\Str::random(10);
}
});
}
public function penerima_bantuan()
{
return $this->hasMany(PenerimaBantuan::class, 'id_tempat_ibadah');
}
public function users()
{
return $this->hasMany(User::class, 'id_tempat_ibadah');
}
}
+6
View File
@@ -23,8 +23,14 @@ class User extends Authenticatable
'email',
'password',
'role',
'id_tempat_ibadah',
];
public function tempatIbadah()
{
return $this->belongsTo(TempatIbadah::class, 'id_tempat_ibadah');
}
/**
* The attributes that should be hidden for serialization.
*
+2
View File
@@ -7,6 +7,8 @@
"license": "MIT",
"require": {
"php": "^8.2",
"barryvdh/laravel-dompdf": "^3.1",
"endroid/qr-code": "^6.0",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1"
},
Generated
+700 -1
View File
@@ -4,8 +4,140 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d5b57ea83b8b7753f51487b1384d9feb",
"content-hash": "46e5face68164b05a599bac22a4fb4ee",
"packages": [
{
"name": "bacon/bacon-qr-code",
"version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2",
"reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2",
"shasum": ""
},
"require": {
"dasprid/enum": "^1.0.3",
"ext-iconv": "*",
"php": "^8.1"
},
"require-dev": {
"phly/keep-a-changelog": "^2.12",
"phpunit/phpunit": "^10.5.11 || ^11.0.4",
"spatie/phpunit-snapshot-assertions": "^5.1.5",
"spatie/pixelmatch-php": "^1.2.0",
"squizlabs/php_codesniffer": "^3.9"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.1.1"
},
"time": "2026-04-05T21:06:35+00:00"
},
{
"name": "barryvdh/laravel-dompdf",
"version": "v3.1.2",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-dompdf.git",
"reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/ee3b72b19ccdf57d0243116ecb2b90261344dedc",
"reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc",
"shasum": ""
},
"require": {
"dompdf/dompdf": "^3.0",
"illuminate/support": "^9|^10|^11|^12|^13.0",
"php": "^8.1"
},
"require-dev": {
"larastan/larastan": "^2.7|^3.0",
"orchestra/testbench": "^7|^8|^9.16|^10|^11.0",
"phpro/grumphp": "^2.5",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"PDF": "Barryvdh\\DomPDF\\Facade\\Pdf",
"Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf"
},
"providers": [
"Barryvdh\\DomPDF\\ServiceProvider"
]
},
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"Barryvdh\\DomPDF\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "A DOMPDF Wrapper for Laravel",
"keywords": [
"dompdf",
"laravel",
"pdf"
],
"support": {
"issues": "https://github.com/barryvdh/laravel-dompdf/issues",
"source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.2"
},
"funding": [
{
"url": "https://fruitcake.nl",
"type": "custom"
},
{
"url": "https://github.com/barryvdh",
"type": "github"
}
],
"time": "2026-02-21T08:51:10+00:00"
},
{
"name": "brick/math",
"version": "0.14.8",
@@ -135,6 +267,56 @@
],
"time": "2024-02-09T16:56:22+00:00"
},
{
"name": "dasprid/enum",
"version": "1.0.7",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
"reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
"reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
"shasum": ""
},
"require": {
"php": ">=7.1 <9.0"
},
"require-dev": {
"phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "*"
},
"type": "library",
"autoload": {
"psr-4": {
"DASPRiD\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "PHP 7.1 enum implementation",
"keywords": [
"enum",
"map"
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
"source": "https://github.com/DASPRiD/Enum/tree/1.0.7"
},
"time": "2025-09-16T12:23:56+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
@@ -377,6 +559,161 @@
],
"time": "2024-02-05T11:56:58+00:00"
},
{
"name": "dompdf/dompdf",
"version": "v3.1.5",
"source": {
"type": "git",
"url": "https://github.com/dompdf/dompdf.git",
"reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
"reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
"shasum": ""
},
"require": {
"dompdf/php-font-lib": "^1.0.0",
"dompdf/php-svg-lib": "^1.0.0",
"ext-dom": "*",
"ext-mbstring": "*",
"masterminds/html5": "^2.0",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"ext-gd": "*",
"ext-json": "*",
"ext-zip": "*",
"mockery/mockery": "^1.3",
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "^3.5",
"symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
},
"suggest": {
"ext-gd": "Needed to process images",
"ext-gmagick": "Improves image processing performance",
"ext-imagick": "Improves image processing performance",
"ext-zlib": "Needed for pdf stream compression"
},
"type": "library",
"autoload": {
"psr-4": {
"Dompdf\\": "src/"
},
"classmap": [
"lib/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1"
],
"authors": [
{
"name": "The Dompdf Community",
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
}
],
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
"homepage": "https://github.com/dompdf/dompdf",
"support": {
"issues": "https://github.com/dompdf/dompdf/issues",
"source": "https://github.com/dompdf/dompdf/tree/v3.1.5"
},
"time": "2026-03-03T13:54:37+00:00"
},
{
"name": "dompdf/php-font-lib",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-font-lib.git",
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a",
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12"
},
"type": "library",
"autoload": {
"psr-4": {
"FontLib\\": "src/FontLib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "The FontLib Community",
"homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
}
],
"description": "A library to read, parse, export and make subsets of different types of font files.",
"homepage": "https://github.com/dompdf/php-font-lib",
"support": {
"issues": "https://github.com/dompdf/php-font-lib/issues",
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.2"
},
"time": "2026-01-20T14:10:26+00:00"
},
{
"name": "dompdf/php-svg-lib",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-svg-lib.git",
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1",
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0",
"sabberworm/php-css-parser": "^8.4 || ^9.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11"
},
"type": "library",
"autoload": {
"psr-4": {
"Svg\\": "src/Svg"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0-or-later"
],
"authors": [
{
"name": "The SvgLib Community",
"homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
}
],
"description": "A library to read, parse and export to PDF SVG files.",
"homepage": "https://github.com/dompdf/php-svg-lib",
"support": {
"issues": "https://github.com/dompdf/php-svg-lib/issues",
"source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2"
},
"time": "2026-01-02T16:01:13+00:00"
},
{
"name": "dragonmantank/cron-expression",
"version": "v3.6.0",
@@ -508,6 +845,78 @@
],
"time": "2025-03-06T22:45:56+00:00"
},
{
"name": "endroid/qr-code",
"version": "6.0.9",
"source": {
"type": "git",
"url": "https://github.com/endroid/qr-code.git",
"reference": "21e888e8597440b2205e2e5c484b6c8e556bcd1a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/endroid/qr-code/zipball/21e888e8597440b2205e2e5c484b6c8e556bcd1a",
"reference": "21e888e8597440b2205e2e5c484b6c8e556bcd1a",
"shasum": ""
},
"require": {
"bacon/bacon-qr-code": "^3.0",
"php": "^8.2"
},
"require-dev": {
"endroid/quality": "dev-main",
"ext-gd": "*",
"khanamiryan/qrcode-detector-decoder": "^2.0.2",
"setasign/fpdf": "^1.8.2"
},
"suggest": {
"ext-gd": "Enables you to write PNG images",
"khanamiryan/qrcode-detector-decoder": "Enables you to use the image validator",
"roave/security-advisories": "Makes sure package versions with known security issues are not installed",
"setasign/fpdf": "Enables you to use the PDF writer"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "6.x-dev"
}
},
"autoload": {
"psr-4": {
"Endroid\\QrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jeroen van den Enden",
"email": "info@endroid.nl"
}
],
"description": "Endroid QR Code",
"homepage": "https://github.com/endroid/qr-code",
"keywords": [
"code",
"endroid",
"php",
"qr",
"qrcode"
],
"support": {
"issues": "https://github.com/endroid/qr-code/issues",
"source": "https://github.com/endroid/qr-code/tree/6.0.9"
},
"funding": [
{
"url": "https://github.com/endroid",
"type": "github"
}
],
"time": "2025-07-13T19:59:45+00:00"
},
{
"name": "fruitcake/php-cors",
"version": "v1.4.0",
@@ -2025,6 +2434,73 @@
],
"time": "2026-03-08T20:05:35+00:00"
},
{
"name": "masterminds/html5",
"version": "2.10.0",
"source": {
"type": "git",
"url": "https://github.com/Masterminds/html5-php.git",
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
"shasum": ""
},
"require": {
"ext-dom": "*",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
},
"autoload": {
"psr-4": {
"Masterminds\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Matt Butcher",
"email": "technosophos@gmail.com"
},
{
"name": "Matt Farina",
"email": "matt@mattfarina.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "An HTML5 parser and serializer.",
"homepage": "http://masterminds.github.io/html5-php",
"keywords": [
"HTML5",
"dom",
"html",
"parser",
"querypath",
"serializer",
"xml"
],
"support": {
"issues": "https://github.com/Masterminds/html5-php/issues",
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
},
"time": "2025-07-25T09:04:22+00:00"
},
{
"name": "monolog/monolog",
"version": "3.10.0",
@@ -3300,6 +3776,86 @@
},
"time": "2025-12-14T04:43:48+00:00"
},
{
"name": "sabberworm/php-css-parser",
"version": "v9.3.0",
"source": {
"type": "git",
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
"reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
"reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "1.4.0",
"phpstan/extension-installer": "1.4.3",
"phpstan/phpstan": "1.12.32 || 2.1.32",
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.8",
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7",
"phpunit/phpunit": "8.5.52",
"rawr/phpunit-data-provider": "3.3.1",
"rector/rector": "1.2.10 || 2.2.8",
"rector/type-perfect": "1.0.0 || 2.1.0",
"squizlabs/php_codesniffer": "4.0.1",
"thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1"
},
"suggest": {
"ext-mbstring": "for parsing UTF-8 CSS"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "9.4.x-dev"
}
},
"autoload": {
"files": [
"src/Rule/Rule.php",
"src/RuleSet/RuleContainer.php"
],
"psr-4": {
"Sabberworm\\CSS\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Raphael Schweikert"
},
{
"name": "Oliver Klee",
"email": "github@oliverklee.de"
},
{
"name": "Jake Hotson",
"email": "jake.github@qzdesign.co.uk"
}
],
"description": "Parser for CSS Files written in PHP",
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"keywords": [
"css",
"parser",
"stylesheet"
],
"support": {
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.3.0"
},
"time": "2026-03-03T17:31:43+00:00"
},
{
"name": "symfony/clock",
"version": "v7.4.8",
@@ -5809,6 +6365,149 @@
],
"time": "2026-03-30T13:44:50+00:00"
},
{
"name": "thecodingmachine/safe",
"version": "v3.4.0",
"source": {
"type": "git",
"url": "https://github.com/thecodingmachine/safe.git",
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19",
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.4",
"phpstan/phpstan": "^2",
"phpunit/phpunit": "^10",
"squizlabs/php_codesniffer": "^3.2"
},
"type": "library",
"autoload": {
"files": [
"lib/special_cases.php",
"generated/apache.php",
"generated/apcu.php",
"generated/array.php",
"generated/bzip2.php",
"generated/calendar.php",
"generated/classobj.php",
"generated/com.php",
"generated/cubrid.php",
"generated/curl.php",
"generated/datetime.php",
"generated/dir.php",
"generated/eio.php",
"generated/errorfunc.php",
"generated/exec.php",
"generated/fileinfo.php",
"generated/filesystem.php",
"generated/filter.php",
"generated/fpm.php",
"generated/ftp.php",
"generated/funchand.php",
"generated/gettext.php",
"generated/gmp.php",
"generated/gnupg.php",
"generated/hash.php",
"generated/ibase.php",
"generated/ibmDb2.php",
"generated/iconv.php",
"generated/image.php",
"generated/imap.php",
"generated/info.php",
"generated/inotify.php",
"generated/json.php",
"generated/ldap.php",
"generated/libxml.php",
"generated/lzf.php",
"generated/mailparse.php",
"generated/mbstring.php",
"generated/misc.php",
"generated/mysql.php",
"generated/mysqli.php",
"generated/network.php",
"generated/oci8.php",
"generated/opcache.php",
"generated/openssl.php",
"generated/outcontrol.php",
"generated/pcntl.php",
"generated/pcre.php",
"generated/pgsql.php",
"generated/posix.php",
"generated/ps.php",
"generated/pspell.php",
"generated/readline.php",
"generated/rnp.php",
"generated/rpminfo.php",
"generated/rrd.php",
"generated/sem.php",
"generated/session.php",
"generated/shmop.php",
"generated/sockets.php",
"generated/sodium.php",
"generated/solr.php",
"generated/spl.php",
"generated/sqlsrv.php",
"generated/ssdeep.php",
"generated/ssh2.php",
"generated/stream.php",
"generated/strings.php",
"generated/swoole.php",
"generated/uodbc.php",
"generated/uopz.php",
"generated/url.php",
"generated/var.php",
"generated/xdiff.php",
"generated/xml.php",
"generated/xmlrpc.php",
"generated/yaml.php",
"generated/yaz.php",
"generated/zip.php",
"generated/zlib.php"
],
"classmap": [
"lib/DateTime.php",
"lib/DateTimeImmutable.php",
"lib/Exceptions/",
"generated/Exceptions/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHP core functions that throw exceptions instead of returning FALSE on error",
"support": {
"issues": "https://github.com/thecodingmachine/safe/issues",
"source": "https://github.com/thecodingmachine/safe/tree/v3.4.0"
},
"funding": [
{
"url": "https://github.com/OskarStark",
"type": "github"
},
{
"url": "https://github.com/shish",
"type": "github"
},
{
"url": "https://github.com/silasjoisten",
"type": "github"
},
{
"url": "https://github.com/staabm",
"type": "github"
}
],
"time": "2026-02-04T18:08:13+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "v2.4.0",
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->foreignId('id_tempat_ibadah')->nullable()->constrained('tempat_ibadah')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropForeign(['id_tempat_ibadah']);
$table->dropColumn('id_tempat_ibadah');
});
}
};
@@ -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('log_distribusi', function (Blueprint $table) {
$table->id();
$table->foreignId('id_penerima')->constrained('penerima_bantuan')->onDelete('cascade');
$table->dateTime('tanggal_diterima');
$table->string('status')->default('Menunggu');
$table->text('foto_penyerahan')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('log_distribusi');
}
};
@@ -0,0 +1,60 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('tempat_ibadah', function (Blueprint $table) {
// Rename kontak_person to kontak_pengurus
$table->renameColumn('kontak_person', 'kontak_pengurus');
});
Schema::table('tempat_ibadah', function (Blueprint $table) {
// Add new columns
$table->string('nama_pengurus')->nullable()->after('kontak_pengurus');
});
Schema::table('penerima_bantuan', function (Blueprint $table) {
// Rename foto_kondisi_rumah to foto (multi-foto, comma-separated)
$table->renameColumn('foto_kondisi_rumah', 'foto');
});
Schema::table('penerima_bantuan', function (Blueprint $table) {
// Change foto column to text for multi-file support
$table->text('foto')->nullable()->change();
// Add new columns
$table->double('jarak_ke_penyalur')->nullable()->after('foto');
$table->string('dokumen_laporan')->nullable()->after('jarak_ke_penyalur');
$table->dateTime('tanggal_pencairan_terakhir')->nullable()->after('dokumen_laporan');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('penerima_bantuan', function (Blueprint $table) {
$table->dropColumn(['jarak_ke_penyalur', 'dokumen_laporan', 'tanggal_pencairan_terakhir']);
});
Schema::table('penerima_bantuan', function (Blueprint $table) {
$table->renameColumn('foto', 'foto_kondisi_rumah');
});
Schema::table('tempat_ibadah', function (Blueprint $table) {
$table->dropColumn(['nama_pengurus']);
});
Schema::table('tempat_ibadah', function (Blueprint $table) {
$table->renameColumn('kontak_pengurus', 'kontak_person');
});
}
};
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('penerima_bantuan', function (Blueprint $table) {
$table->enum('status_persetujuan', ['menunggu', 'disetujui', 'ditolak'])->default('menunggu');
$table->text('alasan_penolakan')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('penerima_bantuan', function (Blueprint $table) {
$table->dropColumn(['status_persetujuan', 'alasan_penolakan']);
});
}
};
@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('users')->where('role', 'admin_ibadah')->update(['role' => 'user']);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('users')->where('role', 'user')->update(['role' => 'admin_ibadah']);
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('tempat_ibadah', function (Blueprint $table) {
$table->string('password')->nullable()->after('radius_meter');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('tempat_ibadah', function (Blueprint $table) {
$table->dropColumn('password');
});
}
};
@@ -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_wargas', function (Blueprint $table) {
$table->id();
$table->string('nama_calon');
$table->integer('jumlah_tanggungan');
$table->string('foto')->nullable();
$table->enum('status_laporan', ['menunggu', 'diproses'])->default('menunggu');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('laporan_wargas');
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('laporan_wargas', function (Blueprint $table) {
$table->text('deskripsi')->nullable()->after('jumlah_tanggungan');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('laporan_wargas', function (Blueprint $table) {
$table->dropColumn('deskripsi');
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('penerima_bantuan', function (Blueprint $table) {
//
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('penerima_bantuan', function (Blueprint $table) {
//
});
}
};
@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('tempat_ibadah', function (Blueprint $table) {
$table->string('kode_lapor')->nullable()->unique()->after('id');
});
Schema::table('laporan_wargas', function (Blueprint $table) {
$table->unsignedBigInteger('id_tempat_ibadah')->nullable()->after('id');
// Foreign key jika menggunakan InnoDB, namun karena mungkin ada data tidak sinkron, biarkan saja index biasa.
$table->index('id_tempat_ibadah');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('laporan_wargas', function (Blueprint $table) {
$table->dropColumn('id_tempat_ibadah');
});
Schema::table('tempat_ibadah', function (Blueprint $table) {
$table->dropColumn('kode_lapor');
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('laporan_wargas', function (Blueprint $table) {
$table->string('alamat')->nullable()->after('jumlah_tanggungan');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('laporan_wargas', function (Blueprint $table) {
$table->dropColumn('alamat');
});
}
};
@@ -7,7 +7,7 @@ use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class SuperAdminSeeder extends Seeder
class AdminSeeder extends Seeder
{
/**
* Run the database seeds.
@@ -15,9 +15,9 @@ class SuperAdminSeeder extends Seeder
public function run(): void
{
User::create([
'name' => env('SUPERADMIN_NAME', 'SuperAdmin'),
'email' => env('SUPERADMIN_EMAIL', 'admin@gmail.com'),
'password' => Hash::make(env('SUPERADMIN_PASSWORD', 'qwerty#123')), // Ganti dengan password yang aman
'name' => env('ADMIN_NAME', 'Admin'),
'email' => env('ADMIN_EMAIL', 'admin@gmail.com'),
'password' => Hash::make(env('ADMIN_PASSWORD', 'qwerty#123')), // Ganti dengan password yang aman
'role' => 'admin', // Menyesuaikan kolom role yang Anda buat di tabel users
]);
}
+2 -2
View File
@@ -13,9 +13,9 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
// Memanggil SuperAdminSeeder
// Memanggil AdminSeeder
$this->call([
SuperAdminSeeder::class,
AdminSeeder::class,
]);
}
}
+3 -3
View File
@@ -39,9 +39,9 @@
</a>
@endif
<x-primary-button class="ms-3">
{{ __('Log in') }}
</x-primary-button>
<button type="submit" class="ms-3 px-6 py-2.5 rounded-full bg-gradient-to-r from-indigo-600 to-purple-600 text-white font-medium hover:shadow-lg hover:shadow-indigo-500/30 hover:-translate-y-0.5 transition-all duration-300">
{{ __('Masuk') }}
</button>
</div>
</form>
</x-guest-layout>
@@ -1 +1 @@
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-100/50 dark:hover:bg-slate-800/50 focus:outline-none focus:bg-slate-100/50 dark:focus:bg-slate-800/50 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
@@ -2,8 +2,8 @@
@php
$classes = ($active ?? false)
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out';
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-500 dark:border-indigo-400 text-sm font-bold leading-5 text-indigo-700 dark:text-indigo-300 focus:outline-none transition duration-150 ease-in-out'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 hover:border-slate-300 dark:hover:border-slate-600 focus:outline-none focus:text-slate-800 dark:focus:text-slate-200 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
@@ -2,8 +2,8 @@
@php
$classes = ($active ?? false)
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out'
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out';
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-500 dark:border-indigo-400 text-start text-base font-bold text-indigo-700 dark:text-indigo-300 bg-indigo-50/50 dark:bg-indigo-900/30 focus:outline-none transition duration-150 ease-in-out'
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 hover:bg-slate-50/50 dark:hover:bg-slate-800/30 hover:border-slate-300 dark:hover:border-slate-600 focus:outline-none transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
+370 -78
View File
@@ -5,96 +5,255 @@
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
<style>
.map-table { width: 100%; border-collapse: collapse; background: white; color: black; margin-top: 20px;}
.map-table th, .map-table td { padding: 12px; border: 1px solid #ddd; text-align: left; }
.map-table th { background: #007bff; color: white; }
.btn-edit { background: #ffc107; color: #000; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; text-decoration: none; font-size: 13px; }
.btn-delete { background: #dc3545; color: #fff; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; text-decoration: none; font-size: 13px; }
.edit-form { background: #fff3cd; padding: 15px; border-radius: 12px; display: none; margin-bottom: 15px; border: 1px solid #ffeeba; color: black; }
</style>
<div class="py-8">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-8">
<div class="bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl overflow-hidden shadow-xl sm:rounded-3xl border border-slate-200/50 dark:border-slate-800/50">
<div class="p-6 md:p-8 text-slate-900 dark:text-slate-100">
@if(session('success'))
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800/50 text-emerald-800 dark:text-emerald-300 px-4 py-3 rounded-2xl mb-8 flex items-center shadow-sm">
<svg class="w-5 h-5 mr-2" 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"></path></svg>
<span class="font-medium">{{ session('success') }}</span>
</div>
@endif
<div id="panel-edit" class="edit-form">
<form action="{{ route('map.edit') }}" method="POST" enctype="multipart/form-data">
<!-- Header & Filter -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4">
<div>
<h3 class="text-2xl font-bold font-outfit text-slate-900 dark:text-white tracking-tight">Data Penerima Bantuan</h3>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">Kelola dan tinjau data penerima bantuan di wilayah Anda.</p>
</div>
<div class="w-full sm:w-auto flex flex-wrap items-center gap-3">
@if(Auth::user()->role === 'admin')
<form method="GET" action="" class="flex items-center w-full sm:w-auto">
<div class="relative w-full sm:w-64">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-4 w-4 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path></svg>
</div>
<select name="filter_penyalur" id="filter_penyalur" class="bg-white dark:bg-slate-800/50 border border-slate-300 text-slate-900 text-sm rounded-xl focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-9 pr-8 py-2.5 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white shadow-sm transition-colors cursor-pointer appearance-none" onchange="this.form.submit()">
<option value="">Semua Penyalur</option>
@foreach($ibadahs as $ib)
<option value="{{ $ib->id }}" {{ request('filter_penyalur') == $ib->id ? 'selected' : '' }}>{{ Str::limit($ib->nama_tempat, 25) }}</option>
@endforeach
</select>
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<svg class="h-4 w-4 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</div>
</div>
</form>
@endif
<button type="button" onclick="opencetakModal()" class="px-5 py-2.5 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white rounded-xl font-medium shadow-lg shadow-blue-500/30 hover:-translate-y-0.5 transition-all flex items-center text-sm whitespace-nowrap">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"></path></svg>
Cetak Kupon
</button>
</div>
</div>
<!-- Edit Form Panel -->
<div id="panel-edit" class="hidden bg-amber-50/80 dark:bg-amber-900/20 backdrop-blur-md border border-amber-200/50 dark:border-amber-700/50 rounded-3xl p-6 md:p-8 mb-8 shadow-lg transition-all duration-300">
<div class="flex items-center mb-6 border-b border-amber-200/50 dark:border-amber-700/50 pb-4">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center text-white shadow-lg shadow-amber-500/30 mr-4 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="M11 5H6a2 2 0 00-2-2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</div>
<h3 class="text-2xl font-bold font-outfit text-slate-900 dark:text-white tracking-tight">Edit Data Penerima Bantuan</h3>
</div>
<form action="{{ route('map.edit') }}" method="POST" enctype="multipart/form-data" class="grid grid-cols-1 md:grid-cols-2 gap-5">
@csrf
<strong>Edit Data Penerima Bantuan:</strong>
<div style="display: flex; gap: 10px; margin-top: 10px; flex-wrap: wrap; align-items: center;">
<input type="hidden" name="id" id="edit-id">
<input type="hidden" name="type" id="edit-type" value="penerima">
<input type="text" name="nama" id="edit-nama" placeholder="Nama KK" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc; color: black;" required>
<input type="text" name="nik_kepala_keluarga" id="edit-nik" placeholder="NIK" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc; color: black;">
<input type="text" name="nomor_kk" id="edit-kk" placeholder="No. KK" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc; color: black;">
<input type="number" name="jumlah_tanggungan" id="edit-tanggungan" placeholder="Jml Tanggungan" style="padding: 8px; width: 150px; border-radius: 4px; border: 1px solid #ccc; color: black;" min="0">
<select name="id_tempat_ibadah" id="edit-ibadah" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc; color: black;">
<input type="hidden" name="id" id="edit-id">
<input type="hidden" name="type" id="edit-type" value="penerima">
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Nama Kepala Keluarga</label>
<input type="text" name="nama" id="edit-nama" class="form-input rounded-xl border-slate-300 dark:border-slate-600 dark:bg-slate-800/50" required>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">NIK</label>
<input type="text" name="nik_kepala_keluarga" id="edit-nik" class="form-input rounded-xl border-slate-300 dark:border-slate-600 dark:bg-slate-800/50">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Nomor KK</label>
<input type="text" name="nomor_kk" id="edit-kk" class="form-input rounded-xl border-slate-300 dark:border-slate-600 dark:bg-slate-800/50">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Jumlah Tanggungan</label>
<input type="number" name="jumlah_tanggungan" id="edit-tanggungan" class="form-input rounded-xl border-slate-300 dark:border-slate-600 dark:bg-slate-800/50" min="0">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Tempat Ibadah / Penyalur</label>
<select name="id_tempat_ibadah" id="edit-ibadah" class="form-input rounded-xl border-slate-300 dark:border-slate-600 dark:bg-slate-800/50">
<option value="">- Pilih Tempat Ibadah -</option>
@foreach($ibadahs as $ib)
<option value="{{ $ib->id }}">{{ $ib->nama_tempat }}</option>
@endforeach
</select>
<input type="text" name="alamat" id="edit-alamat" placeholder="Alamat" style="padding: 8px; width: 250px; border-radius: 4px; border: 1px solid #ccc; color: black;" required>
<label style="display:flex; align-items:center; gap:5px; color:#333; font-weight:bold; font-size:13px;">
Ganti Foto: <input type="file" name="foto_kondisi_rumah" id="edit-foto" accept="image/*" style="color: black; font-size:12px;">
</label>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Alamat Lengkap</label>
<input type="text" name="alamat" id="edit-alamat" class="form-input rounded-xl border-slate-300 dark:border-slate-600 dark:bg-slate-800/50" required>
</div>
<div class="col-span-1 md:col-span-2 mt-2">
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Foto Saat Ini (Centang untuk menghapus):</label>
<div id="current-photos-container" class="bg-white/60 dark:bg-slate-800/60 backdrop-blur-sm p-4 border border-slate-200/50 dark:border-slate-700/50 rounded-2xl flex gap-4 flex-wrap">
<!-- Foto existing dimuat lewat JS -->
</div>
</div>
<button type="submit" style="padding: 8px 15px; background: #28a745; color: white; border: none; border-radius: 4px;">Simpan</button>
<button type="button" onclick="closeEdit()" style="padding: 8px 15px; background: #6c757d; color: white; border: none; border-radius: 4px;">Batal</button>
<div class="col-span-1 md:col-span-2">
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Upload Foto Baru (opsional)</label>
<input type="file" name="foto[]" id="edit-foto" accept="image/*" multiple class="block w-full text-sm text-slate-500 dark:text-slate-400 file:mr-4 file:py-2.5 file:px-5 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100 dark:file:bg-indigo-900/50 dark:file:text-indigo-300 transition-colors bg-white/50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-xl p-1">
</div>
<div class="col-span-1 md:col-span-2 flex justify-end space-x-4 mt-6 pt-4 border-t border-amber-200/50 dark:border-amber-700/50">
<button type="button" onclick="closeEdit()" class="px-6 py-2.5 bg-white/80 dark:bg-slate-700/80 backdrop-blur-sm border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-200 rounded-full hover:bg-slate-50 dark:hover:bg-slate-600 font-medium transition-all shadow-sm">Batal</button>
<button type="submit" class="px-6 py-2.5 bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-400 hover:to-orange-400 text-white rounded-full font-medium shadow-lg shadow-amber-500/30 hover:-translate-y-0.5 transition-all">Simpan Perubahan</button>
</div>
</form>
</div>
<table class="map-table">
<thead>
<tr>
<th>ID</th>
<th>Nama Kepala Keluarga</th>
<th>NIK / KK</th>
<th>Tanggungan</th>
<th>Tempat Ibadah</th>
<th>Alamat Detail</th>
<th>Koordinat (Lat, Lng)</th>
<th>Foto</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
@foreach($penerimas as $r)
<tr>
<td>{{ $r->id }}</td>
<td>{{ $r->nama_kepala_keluarga }}</td>
<td>{{ $r->nik_kepala_keluarga ?? '-' }} / {{ $r->nomor_kk ?? '-' }}</td>
<td>{{ $r->jumlah_tanggungan ?? '0' }}</td>
<td>{{ $r->tempat_ibadah->nama_tempat ?? '-' }}</td>
<td>{{ $r->alamat }}</td>
<td>{{ $r->lat }}, {{ $r->lng }}</td>
<td>
@if($r->foto_kondisi_rumah)
<a href="{{ asset('storage/' . $r->foto_kondisi_rumah) }}" target="_blank" style="color: blue; text-decoration: underline;">Lihat Foto</a>
@else
-
@endif
</td>
<td>
<button class='btn-edit' onclick='openEdit({{ $r->id }}, "{{ addslashes($r->nama_kepala_keluarga) }}", "{{ addslashes($r->alamat) }}", "{{ addslashes($r->nik_kepala_keluarga) }}", "{{ addslashes($r->nomor_kk) }}", "{{ $r->jumlah_tanggungan }}", "{{ $r->id_tempat_ibadah }}")'>Edit</button>
<a href='{{ route("map.delete", ["type" => "penerima", "id" => $r->id]) }}' class='btn-delete' onclick='return confirm("Hapus data penerima ini?")'>Hapus</a>
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- Cards Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
@forelse($penerimas as $index => $r)
@php
$logBulanIni = App\Models\LogDistribusi::where('id_penerima', $r->id)
->whereMonth('tanggal_diterima', now()->month)
->whereYear('tanggal_diterima', now()->year)
->first();
if ($logBulanIni) {
if ($logBulanIni->status == 'Menunggu') {
$status_badge = "<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300'>Menunggu Verifikasi</span>";
} else {
$status_badge = "<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'>Selesai</span>";
}
} else {
if ($r->tanggal_pencairan_terakhir && $r->tanggal_pencairan_terakhir->format('Y-m') === now()->format('Y-m')) {
$status_badge = "<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'>Selesai</span>";
} else {
$status_badge = "<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300'>Belum Cair</span>";
}
}
$penyalur = $r->tempat_ibadah ? $r->tempat_ibadah->nama_tempat : '-';
$jarak = $r->jarak_ke_penyalur !== null ? round($r->jarak_ke_penyalur) . " m" : "-";
$tgl_format = $r->tanggal_pencairan_terakhir ? $r->tanggal_pencairan_terakhir->format('d M Y H:i') : "-";
$dokumen_pdf = $r->dokumen_laporan ? "<a href='".asset('storage/uploads/'.$r->dokumen_laporan)."' target='_blank' class='text-indigo-600 dark:text-indigo-400 hover:underline inline-flex items-center'><svg class='w-4 h-4 mr-1' fill='none' stroke='currentColor' viewBox='0 0 24 24'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'></path></svg>Lihat PDF</a>" : "-";
$id_laporan = $r->id_laporan ?? '-';
$foto_html = "-";
if(!empty($r->foto)) {
$fotos = explode(',', $r->foto);
$links = [];
foreach($fotos as $idx => $f) {
$links[] = "<a href='".asset('storage/uploads/'.$f)."' target='_blank' class='text-indigo-600 dark:text-indigo-400 hover:underline'>Foto ".($idx+1)."</a>";
}
$foto_html = implode(' <span class="text-gray-300 dark:text-gray-600">|</span> ', $links);
}
$status_persetujuan_badge = "";
if ($r->status_persetujuan === 'disetujui') {
$status_persetujuan_badge = "<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'>Disetujui</span>";
} elseif ($r->status_persetujuan === 'ditolak') {
$status_persetujuan_badge = "<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300'>Ditolak</span><div class='text-red-600 dark:text-red-400 text-xs mt-1 font-medium italic'>Alasan: " . ($r->alasan_penolakan ?? '-') . "</div>";
} else {
$status_persetujuan_badge = "<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300'>Menunggu Persetujuan</span>";
}
@endphp
<div class="bg-white/60 dark:bg-slate-800/60 backdrop-blur-md border border-slate-200/80 dark:border-slate-700/80 rounded-3xl shadow-lg shadow-slate-200/50 dark:shadow-slate-900/50 hover:shadow-xl hover:-translate-y-1 transition-all duration-300 flex flex-col overflow-hidden group">
<div class="bg-indigo-50/80 dark:bg-indigo-900/30 px-6 py-5 border-b border-indigo-100 dark:border-indigo-800/30 flex justify-between items-start gap-4 transition-colors">
<div class="flex-grow">
<div class="flex items-center gap-3 mb-2">
<span class="text-xs font-bold bg-indigo-100 text-indigo-700 dark:bg-indigo-800 dark:text-indigo-300 py-1 px-2.5 rounded-lg">#{{ $index + 1 }}</span>
<h4 class="font-bold font-outfit text-slate-900 dark:text-white text-xl">{{ $r->nama_kepala_keluarga }}</h4>
</div>
<div class="text-sm text-slate-500 dark:text-slate-400 font-medium">NIK: <span class="text-slate-700 dark:text-slate-300">{{ $r->nik_kepala_keluarga }}</span> | KK: <span class="text-slate-700 dark:text-slate-300">{{ $r->nomor_kk }}</span></div>
</div>
<div class="flex flex-col items-end gap-2 shrink-0">
{!! str_replace(['rounded-full', 'px-2.5'], ['rounded-lg', 'px-3'], $status_persetujuan_badge) !!}
{!! str_replace(['rounded-full', 'px-2.5'], ['rounded-lg', 'px-3'], $status_badge) !!}
</div>
</div>
<div class="p-6 flex-grow text-sm space-y-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
<div class="flex flex-col">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1">Tanggungan</span>
<span class="text-slate-900 dark:text-slate-200 font-semibold">{{ $r->jumlah_tanggungan }} Orang</span>
</div>
<div class="flex flex-col">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1">Penyalur Bantuan</span>
<span class="text-slate-900 dark:text-slate-200 font-semibold">{{ $penyalur }}</span>
</div>
<div class="flex flex-col">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1">Jarak (Estimasi)</span>
<span class="text-indigo-600 dark:text-indigo-400 font-bold">{{ $jarak }}</span>
</div>
<div class="flex flex-col">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1">Tgl Cair Terakhir</span>
<span class="text-emerald-600 dark:text-emerald-400 font-bold">{{ $tgl_format }}</span>
</div>
</div>
<div class="border-t border-slate-100 dark:border-slate-700/50 pt-4">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1 block">Alamat</span>
<span class="text-slate-900 dark:text-slate-200 leading-relaxed block">{{ $r->alamat }}</span>
<span class="text-xs text-slate-400 dark:text-slate-500 mt-2 font-mono bg-slate-100 dark:bg-slate-900/50 py-1 px-2 rounded w-max inline-block border border-slate-200 dark:border-slate-800">Koordinat: {{ $r->lat }}, {{ $r->lng }}</span>
</div>
<div class="border-t border-slate-100 dark:border-slate-700/50 pt-4 grid grid-cols-2 gap-6">
<div class="flex flex-col">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1.5">Dokumen</span>
<span class="font-medium">{!! $dokumen_pdf !!}</span>
</div>
<div class="flex flex-col">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1.5">Foto Bukti</span>
<span class="font-medium">{!! $foto_html !!}</span>
</div>
</div>
</div>
<div class="px-6 py-4 bg-slate-50/50 dark:bg-slate-800/30 border-t border-slate-200/50 dark:border-slate-700/50 flex justify-end gap-3 flex-wrap">
@if (Auth::user()->role === 'admin' && $r->status_persetujuan !== 'disetujui' && $r->status_persetujuan !== 'ditolak')
<form action="{{ route('persetujuan.update', $r->id) }}" method="POST" class="inline">
@csrf
<input type="hidden" name="status_persetujuan" value="disetujui">
<button type="submit" class="inline-flex items-center px-4 py-2 border border-emerald-200 dark:border-emerald-800/50 text-sm font-semibold rounded-xl text-emerald-800 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-900/30 hover:bg-emerald-100 dark:hover:bg-emerald-900/50 focus:outline-none transition-all shadow-sm hover:-translate-y-0.5" title="Setujui">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
Setujui
</button>
</form>
<button type="button" class="inline-flex items-center px-4 py-2 border border-rose-200 dark:border-rose-800/50 text-sm font-semibold rounded-xl text-rose-800 dark:text-rose-300 bg-rose-50 dark:bg-rose-900/30 hover:bg-rose-100 dark:hover:bg-rose-900/50 focus:outline-none transition-all shadow-sm hover:-translate-y-0.5" title="Tolak" onclick="openTolakModal({{ $r->id }})">
<svg class="w-4 h-4 mr-1.5" 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"></path></svg>
Tolak
</button>
@endif
<button class='inline-flex items-center px-4 py-2 border border-amber-200 dark:border-amber-700/50 text-sm font-semibold rounded-xl text-amber-800 dark:text-amber-300 bg-amber-50 dark:bg-amber-900/30 hover:bg-amber-100 dark:hover:bg-amber-900/50 focus:outline-none transition-all shadow-sm hover:-translate-y-0.5' title='Edit' onclick='openEdit({{ $r->id }}, "{{ addslashes($r->nama_kepala_keluarga) }}", "{{ addslashes($r->alamat) }}", "{{ addslashes($r->nik_kepala_keluarga) }}", "{{ addslashes($r->nomor_kk) }}", "{{ $r->jumlah_tanggungan }}", "{{ $r->id_tempat_ibadah }}", `{{ $r->foto }}`)'>
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
Edit
</button>
<a href='{{ route("map.delete", ["type" => "penerima", "id" => $r->id]) }}' class='inline-flex items-center px-4 py-2 border border-rose-200 dark:border-rose-700/50 text-sm font-semibold rounded-xl text-rose-700 dark:text-rose-300 bg-rose-50 dark:bg-rose-900/30 hover:bg-rose-100 dark:hover:bg-rose-900/50 focus:outline-none transition-all shadow-sm hover:-translate-y-0.5' title='Hapus' onclick='return confirm("Hapus data penerima ini?")'>
<svg class="w-4 h-4 mr-1.5" 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"></path></svg>
Hapus
</a>
</div>
</div>
@empty
<div class="col-span-1 lg:col-span-2 py-16 text-center bg-white/40 dark:bg-slate-800/40 backdrop-blur-sm rounded-3xl border border-slate-200/50 dark:border-slate-700/50 shadow-sm">
<div class="w-20 h-20 mx-auto bg-slate-100 dark:bg-slate-700 rounded-full flex items-center justify-center mb-4 shadow-inner">
<svg class="w-10 h-10 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
</div>
<p class="text-slate-600 dark:text-slate-400 font-bold text-lg">Data penerima bantuan tidak ditemukan.</p>
</div>
@endforelse
</div>
<script>
function openEdit(id, nama, alamat, nik, kk, tanggungan, ibadah_id) {
document.getElementById('panel-edit').style.display = 'block';
function openEdit(id, nama, alamat, nik, kk, tanggungan, ibadah_id, fotoStr) {
var panel = document.getElementById('panel-edit');
panel.classList.remove('hidden');
document.getElementById('edit-id').value = id;
document.getElementById('edit-nama').value = nama;
document.getElementById('edit-alamat').value = alamat;
@@ -102,13 +261,146 @@
document.getElementById('edit-kk').value = kk || '';
document.getElementById('edit-tanggungan').value = tanggungan || '';
document.getElementById('edit-ibadah').value = ibadah_id || '';
window.scrollTo(0,0);
// Render current photos with delete checkbox
const container = document.getElementById('current-photos-container');
if(fotoStr && fotoStr.trim() !== '') {
const fotos = fotoStr.split(',');
let htmlStr = '';
fotos.forEach(f => {
const url = '{{ asset("storage/uploads/") }}/' + f;
htmlStr += `
<div class="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden w-24 flex flex-col shadow-sm">
<img src="${url}" class="w-24 h-24 object-cover bg-slate-100 dark:bg-slate-900">
<label class="text-xs font-semibold text-center py-2 bg-slate-50 dark:bg-slate-800 cursor-pointer hover:bg-rose-50 dark:hover:bg-rose-900/20 hover:text-rose-600 transition-colors border-t border-slate-200 dark:border-slate-700 block">
<input type="checkbox" name="hapus_foto[]" value="${f}" class="mr-1 rounded border-slate-300 text-rose-600 focus:ring-rose-500"> Hapus
</label>
</div>
`;
});
container.innerHTML = htmlStr;
} else {
container.innerHTML = '<span class="text-sm text-slate-400 dark:text-slate-500 italic font-medium">Belum ada foto yang diunggah.</span>';
}
panel.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function closeEdit() {
document.getElementById('panel-edit').classList.add('hidden');
}
function closeEdit() { document.getElementById('panel-edit').style.display = 'none'; }
</script>
</div>
</div>
</div>
</div>
<!-- Modal Penolakan -->
<div id="tolakModal" class="hidden fixed inset-0 bg-slate-900/50 backdrop-blur-md z-50 justify-center items-center p-4">
<div class="bg-white dark:bg-slate-800 rounded-3xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all border border-slate-200 dark:border-slate-700">
<div class="px-6 py-4 border-b border-slate-100 dark:border-slate-700 flex justify-between items-center bg-slate-50 dark:bg-slate-800/50">
<h3 class="text-lg font-bold font-outfit text-slate-900 dark:text-white">Alasan Penolakan</h3>
<button onclick="closeTolakModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 bg-white dark:bg-slate-700 p-1 rounded-full shadow-sm">
<svg class="w-5 h-5" 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"></path></svg>
</button>
</div>
<form id="formTolak" method="POST" class="p-6">
@csrf
<input type="hidden" name="status_persetujuan" value="ditolak">
<div class="mb-5">
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Pilih alasan penolakan berkas pengajuan ini:</label>
<select name="alasan_penolakan" required class="form-input rounded-xl border-slate-300 dark:border-slate-600 dark:bg-slate-800/50 w-full">
<option value="">-- Pilih Alasan --</option>
<option value="Data tidak valid / fiktif">Data tidak valid / fiktif</option>
<option value="Sudah terdaftar sebelumnya">Sudah terdaftar sebelumnya</option>
<option value="Dokumen investigasi tidak lengkap atau salah">Dokumen investigasi tidak lengkap atau salah</option>
<option value="Kondisi ekonomi dianggap masih mampu">Kondisi ekonomi dianggap masih mampu</option>
<option value="Lainnya">Lainnya</option>
</select>
</div>
<div class="flex justify-end gap-3 mt-8">
<button type="button" onclick="closeTolakModal()" class="px-5 py-2.5 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-200 rounded-full hover:bg-slate-50 dark:hover:bg-slate-600 font-medium transition-colors shadow-sm">Batal</button>
<button type="submit" class="px-5 py-2.5 bg-gradient-to-r from-rose-500 to-red-600 hover:from-rose-600 hover:to-red-700 text-white rounded-full font-medium shadow-lg shadow-rose-500/30 transition-all">Tolak Pengajuan</button>
</div>
</form>
</div>
</div>
<script>
function openTolakModal(id) {
document.getElementById('formTolak').action = "/persetujuan/" + id;
document.getElementById('tolakModal').classList.remove('hidden');
document.getElementById('tolakModal').classList.add('flex');
}
function closeTolakModal() {
document.getElementById('tolakModal').classList.remove('flex');
document.getElementById('tolakModal').classList.add('hidden');
}
</script>
<!-- Modal Cetak Kupon Massal -->
<div id="cetakModal" class="hidden fixed inset-0 bg-slate-900/50 backdrop-blur-md z-50 justify-center items-center p-4">
<div class="bg-white dark:bg-slate-800 rounded-3xl shadow-2xl w-full max-w-2xl overflow-hidden transform transition-all flex flex-col max-h-[90vh] border border-slate-200 dark:border-slate-700">
<div class="px-6 py-5 border-b border-slate-100 dark:border-slate-700 flex justify-between items-center bg-slate-50/80 dark:bg-slate-800/50 backdrop-blur-sm">
<h3 class="text-xl font-bold font-outfit text-slate-900 dark:text-white flex items-center tracking-tight">
<div class="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center mr-3">
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"></path></svg>
</div>
Pilih Kupon untuk Dicetak
</h3>
<button onclick="closecetakModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 bg-white dark:bg-slate-700 p-1.5 rounded-full shadow-sm transition-colors">
<svg class="w-5 h-5" 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"></path></svg>
</button>
</div>
<form id="formcetak" action="{{ route('kupon.cetak') }}" method="POST" target="_blank" class="flex flex-col flex-grow overflow-hidden">
@csrf
<div class="p-6 bg-white dark:bg-slate-800 overflow-y-auto flex-grow">
<div class="mb-5 flex flex-col sm:flex-row justify-between sm:items-center gap-3">
<span class="text-sm text-slate-600 dark:text-slate-400 font-medium">Pilih penerima bantuan (yang sudah disetujui) yang kuponnya ingin dicetak.</span>
<button type="button" onclick="selectAllKupon()" class="text-sm text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 font-semibold bg-indigo-50 hover:bg-indigo-100 dark:bg-indigo-900/30 dark:hover:bg-indigo-900/50 px-4 py-2 rounded-xl transition-colors shadow-sm whitespace-nowrap">Pilih Semua</button>
</div>
<div class="space-y-3">
@foreach($penerimas->where('status_persetujuan', 'disetujui') as $p)
<label class="flex items-center p-4 border border-slate-200 dark:border-slate-700 rounded-2xl hover:bg-slate-50 dark:hover:bg-slate-700/50 cursor-pointer transition-all shadow-sm">
<input type="checkbox" name="penerima_ids[]" value="{{ $p->id }}" class="kupon-checkbox w-5 h-5 text-indigo-600 border-slate-300 rounded focus:ring-indigo-500 dark:bg-slate-800 dark:border-slate-600 dark:checked:bg-indigo-500">
<div class="ml-4 flex-grow">
<span class="block text-sm font-bold text-slate-900 dark:text-white">{{ $p->nama_kepala_keluarga }}</span>
<span class="block text-xs font-medium text-slate-500 dark:text-slate-400 mt-0.5">NIK: {{ substr($p->nik_kepala_keluarga ?? '', 0, 6) }}********** <span class="mx-1 text-slate-300 dark:text-slate-600">|</span> Penyalur: <span class="text-slate-700 dark:text-slate-300">{{ $p->tempat_ibadah ? $p->tempat_ibadah->nama_tempat : '-' }}</span></span>
</div>
</label>
@endforeach
</div>
</div>
<div class="px-6 py-4 border-t border-slate-100 dark:border-slate-700 flex justify-end gap-3 bg-slate-50/80 dark:bg-slate-800/50 backdrop-blur-sm">
<button type="button" onclick="closecetakModal()" class="px-5 py-2.5 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-200 rounded-full hover:bg-slate-50 dark:hover:bg-slate-600 font-medium transition-all shadow-sm">Batal</button>
<button type="submit" onclick="closecetakModal()" class="px-5 py-2.5 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white rounded-full font-medium shadow-lg shadow-blue-500/30 hover:-translate-y-0.5 transition-all flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"></path></svg>
Cetak Terpilih
</button>
</div>
</form>
</div>
</div>
<script>
function opencetakModal() {
document.getElementById('cetakModal').classList.remove('hidden');
document.getElementById('cetakModal').classList.add('flex');
}
function closecetakModal() {
document.getElementById('cetakModal').classList.remove('flex');
document.getElementById('cetakModal').classList.add('hidden');
}
function selectAllKupon() {
const checkboxes = document.querySelectorAll('.kupon-checkbox');
const allChecked = Array.from(checkboxes).every(c => c.checked);
checkboxes.forEach(c => c.checked = !allChecked);
}
</script>
</x-app-layout>
+178 -88
View File
@@ -5,97 +5,187 @@
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
<style>
.map-table { width: 100%; border-collapse: collapse; background: white; color: black; margin-top: 20px;}
.map-table th, .map-table td { padding: 12px; border: 1px solid #ddd; text-align: left; }
.map-table th { background: #007bff; color: white; }
.btn-edit { background: #ffc107; color: #000; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; text-decoration: none; font-size: 13px; }
.btn-delete { background: #dc3545; color: #fff; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; text-decoration: none; font-size: 13px; }
.edit-form { background: #fff3cd; padding: 15px; border-radius: 12px; display: none; margin-bottom: 15px; border: 1px solid #ffeeba; color: black; }
</style>
<div id="panel-edit" class="edit-form">
<form action="{{ route('map.edit') }}" method="POST">
@csrf
<strong>Edit Data Tempat Ibadah:</strong>
<div style="display: flex; gap: 10px; margin-top: 10px; flex-wrap: wrap; align-items: center;">
<input type="hidden" name="id" id="edit-id">
<input type="hidden" name="type" id="edit-type" value="ibadah">
<input type="text" name="nama" id="edit-nama" placeholder="Nama Tempat" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc; color: black;" required>
<select name="jenis_tempat_ibadah" id="edit-jenis" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc; color: black;">
<option value="">- Pilih Jenis -</option>
<option value="Masjid">Masjid</option>
<option value="Gereja Katolik">Gereja Katolik</option>
<option value="Gereja Protestan">Gereja Protestan</option>
<option value="Vihara">Vihara</option>
<option value="Pura">Pura</option>
<option value="Klenteng">Klenteng</option>
</select>
<input type="text" name="kontak_person" id="edit-kontak" placeholder="Kontak Person" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc; color: black;">
<input type="text" name="alamat" id="edit-alamat" placeholder="Alamat" style="padding: 8px; width: 350px; border-radius: 4px; border: 1px solid #ccc; color: black;" required>
<input type="number" name="radius_meter" id="edit-radius" placeholder="Radius (Meter)" style="padding: 8px; width: 130px; border-radius: 4px; border: 1px solid #ccc; color: black;" min="0" required>
<button type="submit" style="padding: 8px 15px; background: #28a745; color: white; border: none; border-radius: 4px;">Simpan</button>
<button type="button" onclick="closeEdit()" style="padding: 8px 15px; background: #6c757d; color: white; border: none; border-radius: 4px;">Batal</button>
</div>
</form>
<div class="py-8">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-8">
<!-- Description Card -->
<div class="bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl overflow-hidden shadow-xl sm:rounded-3xl p-6 md:p-8 border border-slate-200/50 dark:border-slate-800/50">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-1">Data Tempat Ibadah & Penyalur</h3>
<p class="text-gray-500 dark:text-gray-400 text-sm">
Kelola data tempat ibadah yang menjadi mitra penyalur bantuan.
</p>
</div>
<table class="map-table">
<thead>
<tr>
<th>ID</th>
<th>Nama Tempat Ibadah</th>
<th>Jenis</th>
<th>Kontak Person</th>
<th>Alamat Detail</th>
<th>Radius Layan (Meter)</th>
<th>Koordinat (Lat, Lng)</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
@foreach($ibadahs as $ib)
<tr>
<td>{{ $ib->id }}</td>
<td>{{ $ib->nama_tempat }}</td>
<td>{{ $ib->jenis_tempat_ibadah ?? '-' }}</td>
<td>{{ $ib->kontak_person ?? '-' }}</td>
<td>{{ $ib->alamat }}</td>
<td>{{ $ib->radius_meter }}</td>
<td>{{ $ib->lat }}, {{ $ib->lng }}</td>
<td>
<button class='btn-edit' onclick='openEdit({{ $ib->id }}, "{{ addslashes($ib->nama_tempat) }}", "{{ addslashes($ib->alamat) }}", {{ $ib->radius_meter }}, "{{ addslashes($ib->jenis_tempat_ibadah) }}", "{{ addslashes($ib->kontak_person) }}")'>Edit</button>
<a href='{{ route("map.delete", ["type" => "ibadah", "id" => $ib->id]) }}' class='btn-delete' onclick='return confirm("Hapus data tempat ibadah ini?")'>Hapus</a>
</td>
</tr>
@endforeach
</tbody>
</table>
<script>
function openEdit(id, nama, alamat, radius, jenis, kontak) {
document.getElementById('panel-edit').style.display = 'block';
document.getElementById('edit-id').value = id;
document.getElementById('edit-nama').value = nama;
document.getElementById('edit-alamat').value = alamat;
document.getElementById('edit-radius').value = radius;
document.getElementById('edit-jenis').value = jenis;
document.getElementById('edit-kontak').value = kontak;
window.scrollTo(0,0);
}
function closeEdit() { document.getElementById('panel-edit').style.display = 'none'; }
</script>
</div>
</div>
<!-- Edit Form Panel -->
<div id="panel-edit" class="hidden bg-amber-50/80 dark:bg-amber-900/20 backdrop-blur-md border border-amber-200/50 dark:border-amber-700/50 rounded-3xl p-6 md:p-8 shadow-lg transition-all duration-300">
<div class="flex items-center mb-6 border-b border-amber-200/50 dark:border-amber-700/50 pb-4">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center text-white shadow-lg shadow-amber-500/30 mr-4 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="M11 5H6a2 2 0 00-2-2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</div>
<h3 class="text-2xl font-bold font-outfit text-slate-900 dark:text-white tracking-tight">Edit Data Tempat Ibadah</h3>
</div>
<form action="{{ route('map.edit') }}" method="POST" class="grid grid-cols-1 md:grid-cols-2 gap-4">
@csrf
<input type="hidden" name="id" id="edit-id">
<input type="hidden" name="type" id="edit-type" value="ibadah">
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Nama Tempat</label>
<input type="text" name="nama" id="edit-nama" class="form-input rounded-xl border-slate-300 dark:border-slate-600 dark:bg-slate-800/50" required>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Jenis</label>
<select name="jenis_tempat_ibadah" id="edit-jenis" class="form-input rounded-xl border-slate-300 dark:border-slate-600 dark:bg-slate-800/50">
<option value="">- Pilih Jenis -</option>
<option value="Masjid">Masjid</option>
<option value="Gereja Katolik">Gereja Katolik</option>
<option value="Gereja Protestan">Gereja Protestan</option>
<option value="Vihara">Vihara</option>
<option value="Pura">Pura</option>
<option value="Klenteng">Klenteng</option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Nama Pengurus</label>
<input type="text" name="nama_pengurus" id="edit-nama-pengurus" class="form-input rounded-xl border-slate-300 dark:border-slate-600 dark:bg-slate-800/50">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Kontak Pengurus</label>
<input type="text" name="kontak_pengurus" id="edit-kontak-pengurus" class="form-input rounded-xl border-slate-300 dark:border-slate-600 dark:bg-slate-800/50">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Radius (Meter)</label>
<input type="number" name="radius_meter" id="edit-radius" class="form-input rounded-xl border-slate-300 dark:border-slate-600 dark:bg-slate-800/50" min="0" required>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Alamat</label>
<input type="text" name="alamat" id="edit-alamat" class="form-input rounded-xl border-slate-300 dark:border-slate-600 dark:bg-slate-800/50" required>
</div>
<div class="col-span-1 md:col-span-2 flex justify-end space-x-4 mt-6 pt-4 border-t border-amber-200/50 dark:border-amber-700/50">
<button type="button" onclick="closeEdit()" class="px-6 py-2.5 bg-white/80 dark:bg-slate-700/80 backdrop-blur-sm border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-200 rounded-full hover:bg-slate-50 dark:hover:bg-slate-600 font-medium transition-all shadow-sm">Batal</button>
<button type="submit" class="px-6 py-2.5 bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-400 hover:to-orange-400 text-white rounded-full font-medium shadow-lg shadow-amber-500/30 hover:-translate-y-0.5 transition-all">Simpan Perubahan</button>
</div>
</form>
</div>
<!-- Cards Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
@forelse($ibadahs as $index => $ib)
<div class="bg-white/60 dark:bg-slate-800/60 backdrop-blur-md border border-slate-200/80 dark:border-slate-700/80 rounded-3xl shadow-lg shadow-slate-200/50 dark:shadow-slate-900/50 hover:shadow-xl hover:-translate-y-1 transition-all duration-300 flex flex-col overflow-hidden group">
<div class="bg-indigo-50/80 dark:bg-indigo-900/30 px-6 py-5 border-b border-indigo-100 dark:border-indigo-800/30 flex justify-between items-start gap-4 transition-colors">
<div class="flex-grow">
<div class="flex items-center gap-3 mb-2">
<span class="text-xs font-bold bg-indigo-100 text-indigo-700 dark:bg-indigo-800 dark:text-indigo-300 py-1 px-2.5 rounded-lg">#{{ $index + 1 }}</span>
<h4 class="font-bold font-outfit text-slate-900 dark:text-white text-xl">{{ $ib->nama_tempat }}</h4>
</div>
<div class="text-sm font-medium"><span class="inline-block px-3 py-1 bg-white/60 dark:bg-slate-800/60 text-slate-700 dark:text-slate-300 rounded-full border border-slate-200 dark:border-slate-700 shadow-sm">{{ $ib->jenis_tempat_ibadah ?? 'Lainnya' }}</span></div>
</div>
</div>
<div class="p-6 flex-grow text-sm space-y-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
<div class="flex flex-col">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1">Nama Pengurus</span>
<span class="text-slate-900 dark:text-slate-200 font-semibold">{{ $ib->nama_pengurus ?? '-' }}</span>
</div>
<div class="flex flex-col">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1">Kontak Pengurus</span>
<span class="text-slate-900 dark:text-slate-200 font-semibold">{{ $ib->kontak_pengurus ?? '-' }}</span>
</div>
<div class="flex flex-col">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1">Radius Pelayanan</span>
<span class="text-indigo-600 dark:text-indigo-400 font-bold">{{ $ib->radius_meter }} m</span>
</div>
<div class="flex flex-col">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1">Koordinat</span>
<span class="text-slate-900 dark:text-slate-200 font-medium">{{ $ib->lat }}, {{ $ib->lng }}</span>
</div>
</div>
<div class="border-t border-slate-100 dark:border-slate-700/50 pt-4">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1 block">Alamat</span>
<span class="text-slate-900 dark:text-slate-200 leading-relaxed block">{{ $ib->alamat }}</span>
</div>
@if(Auth::user()->role !== 'user')
<div class="border-t border-slate-100 dark:border-slate-700/50 pt-4 grid grid-cols-1 sm:grid-cols-2 gap-6">
<div class="flex flex-col">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1">Email (Akun Penyalur)</span>
<span class="text-slate-900 dark:text-slate-200 font-mono text-xs bg-slate-100 dark:bg-slate-900/50 py-1.5 px-3 rounded-lg w-max border border-slate-200 dark:border-slate-800">
@if(isset($adminUsers[$ib->id]))
{{ $adminUsers[$ib->id]->email }}
@else
-
@endif
</span>
</div>
<div class="flex flex-col">
<span class="text-slate-500 dark:text-slate-400 font-medium mb-1">Password</span>
@if(isset($adminUsers[$ib->id]))
@if(\Illuminate\Support\Facades\Hash::check('password123', $adminUsers[$ib->id]->password))
<span class="text-slate-900 dark:text-slate-200 font-mono text-xs bg-slate-100 dark:bg-slate-900/50 py-1.5 px-3 rounded-lg w-max border border-slate-200 dark:border-slate-800">password123</span>
@else
<span class="text-emerald-700 dark:text-emerald-400 text-xs bg-emerald-50 dark:bg-emerald-900/30 py-1.5 px-3 rounded-lg w-max flex items-center gap-1.5 font-bold border border-emerald-200 dark:border-emerald-800">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
Telah Diubah (Rahasia)
</span>
@endif
@else
<span class="text-slate-900 dark:text-slate-200 font-mono text-xs bg-slate-100 dark:bg-slate-900/50 py-1.5 px-3 rounded-lg w-max border border-slate-200 dark:border-slate-800">-</span>
@endif
</div>
</div>
@endif
</div>
<div class="px-6 py-4 bg-slate-50/50 dark:bg-slate-800/30 border-t border-slate-200/50 dark:border-slate-700/50 flex justify-end gap-3 flex-wrap">
<button class='inline-flex items-center px-4 py-2 border border-amber-200 dark:border-amber-700 text-sm font-semibold rounded-xl text-amber-800 dark:text-amber-300 bg-amber-50 dark:bg-amber-900/30 hover:bg-amber-100 dark:hover:bg-amber-900/50 focus:outline-none transition-all shadow-sm hover:-translate-y-0.5' title='Edit' onclick='openEdit({{ $ib->id }}, "{{ addslashes($ib->nama_tempat) }}", "{{ addslashes($ib->alamat) }}", {{ $ib->radius_meter }}, "{{ addslashes($ib->jenis_tempat_ibadah) }}", "{{ addslashes($ib->nama_pengurus) }}", "{{ addslashes($ib->kontak_pengurus) }}")'>
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
Edit
</button>
@if(Auth::user()->role !== 'user')
<a href='{{ route("map.delete", ["type" => "ibadah", "id" => $ib->id]) }}' class='inline-flex items-center px-4 py-2 border border-rose-200 dark:border-rose-700 text-sm font-semibold rounded-xl text-rose-700 dark:text-rose-300 bg-rose-50 dark:bg-rose-900/30 hover:bg-rose-100 dark:hover:bg-rose-900/50 focus:outline-none transition-all shadow-sm hover:-translate-y-0.5' title='Hapus' onclick='return confirm("Hapus data ini?")'>
<svg class="w-4 h-4 mr-1.5" 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"></path></svg>
Hapus
</a>
@endif
</div>
</div>
@empty
<div class="col-span-1 lg:col-span-2 py-16 text-center bg-white/40 dark:bg-slate-800/40 backdrop-blur-sm rounded-3xl border border-slate-200/50 dark:border-slate-700/50 shadow-sm">
<div class="w-20 h-20 mx-auto bg-slate-100 dark:bg-slate-700 rounded-full flex items-center justify-center mb-4 shadow-inner">
<svg class="w-10 h-10 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path></svg>
</div>
<p class="text-slate-600 dark:text-slate-400 font-bold text-lg">Data tempat ibadah tidak ditemukan.</p>
</div>
@endforelse
</div>
<script>
function openEdit(id, nama, alamat, radius, jenis, nama_pengurus, kontak_pengurus) {
var panel = document.getElementById('panel-edit');
panel.classList.remove('hidden');
document.getElementById('edit-id').value = id;
document.getElementById('edit-nama').value = nama;
document.getElementById('edit-alamat').value = alamat;
document.getElementById('edit-radius').value = radius;
document.getElementById('edit-jenis').value = jenis;
document.getElementById('edit-nama-pengurus').value = nama_pengurus || '';
document.getElementById('edit-kontak-pengurus').value = kontak_pengurus || '';
panel.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function closeEdit() {
document.getElementById('panel-edit').classList.add('hidden');
}
</script>
</div>
</div>
</x-app-layout>
+426 -147
View File
@@ -5,196 +5,408 @@
</h2>
</x-slot>
<div class="py-12">
<div class="py-8">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
<div class="bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl overflow-hidden shadow-2xl sm:rounded-3xl border border-slate-200/50 dark:border-slate-800/50">
<div class="p-6 sm:p-10 text-slate-900 dark:text-slate-100">
<!-- Map Interface Start -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
#map { height: 450px; border-radius: 12px; margin-bottom: 20px; border: 2px solid #333; z-index: 1; }
.panel { background: #f4f7f6; padding: 20px; border-radius: 12px; margin-bottom: 15px; color: black;}
.edit-form { background: #fff3cd; padding: 15px; border-radius: 12px; display: none; margin-bottom: 15px; border: 1px solid #ffeeba; color: black; }
#map { z-index: 1; }
.leaflet-popup-content-wrapper { border-radius: 1rem; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); border: 1px solid rgba(255,255,255,0.2); backdrop-filter: blur(8px); }
.leaflet-popup-content { text-align: center; color: #0f172a; font-family: 'Inter', ui-sans-serif, system-ui, sans-serif; font-weight: 500;}
.popup-slider { width: 100%; margin-top: 10px; accent-color: #6366f1; }
.map-table { width: 100%; border-collapse: collapse; background: white; color: black; }
.map-table th, .map-table td { padding: 12px; border: 1px solid #ddd; text-align: left; }
.map-table th { background: #007bff; color: white; }
.btn-edit { background: #ffc107; color: #000; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; text-decoration: none; font-size: 13px; }
.btn-delete { background: #dc3545; color: #fff; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; text-decoration: none; font-size: 13px; }
.leaflet-popup-content { text-align: center; color: black; }
.popup-slider { width: 100%; }
/* Ubah kursor peta menjadi crosshair untuk menandakan mode tambah titik */
/* Crosshair when adding point */
.mode-tambah .leaflet-container { cursor: crosshair !important; }
/* Custom form inputs to match Tailwind forms plugin if not installed */
.form-input { display: block; width: 100%; border-radius: 0.75rem; border: 1px solid #e2e8f0; padding: 0.75rem 1rem; font-size: 0.875rem; line-height: 1.25rem; transition: all 0.2s ease-in-out; background-color: rgba(255, 255, 255, 0.5); color: #0f172a; backdrop-filter: blur(8px); }
.form-input:focus { outline: none; border-color: #6366f1; box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15); background-color: #fff; }
.dark .form-input { background-color: rgba(30, 41, 59, 0.5); border-color: #334155; color: #f8fafc; }
.dark .form-input:focus { border-color: #818cf8; box-shadow: 0 0 0 4px rgba(129, 140, 248, 0.15); background-color: #1e293b; }
</style>
<div class="panel">
<strong style="display: block; margin-bottom: 10px;">Tambah Titik Baru:</strong>
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
<input type="text" id="nama" placeholder="Masukkan Nama..." style="padding: 10px; border-radius: 6px; border: 1px solid #ccc; color: black; flex-grow: 1; max-width: 300px;">
<input type="hidden" id="mode" value="">
<button type="button" id="btn-mode-ibadah" onclick="setMode('ibadah')" style="padding: 10px 15px; background: #e2e8f0; color: #333; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; transition: 0.3s;">
🏛️ Tempat Ibadah
</button>
<button type="button" id="btn-mode-penerima" onclick="setMode('penerima')" style="padding: 10px 15px; background: #e2e8f0; color: #333; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; transition: 0.3s;">
🏠 Penerima Bantuan
</button>
@php
$total_penerima = $penerimas->count();
$sudah_cair = 0;
$belum_cair = 0;
$current_month = now()->format('Y-m');
foreach($penerimas as $p) {
if ($p->tanggal_pencairan_terakhir && $p->tanggal_pencairan_terakhir->format('Y-m') === $current_month) {
$sudah_cair++;
} else {
$belum_cair++;
}
}
@endphp
<!-- Top Action Bar -->
<div class="flex flex-col lg:flex-row lg:items-stretch gap-6 mb-8">
<!-- Add Button -->
<button id="btn-toggle-form" class="w-full lg:w-1/4 flex-shrink-0 flex items-center justify-center px-6 py-4 border border-transparent text-base font-semibold rounded-2xl shadow-lg shadow-indigo-500/30 text-white bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 hover:-translate-y-1 transition-all duration-300">
<svg id="btn-icon-plus" class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
<svg id="btn-icon-minus" class="w-6 h-6 mr-2 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"></path></svg>
<span id="btn-text">Tambah Data Baru</span>
</button>
<!-- Stats Panel -->
<div class="flex-grow flex bg-white/50 dark:bg-slate-800/50 backdrop-blur-md rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden divide-x divide-slate-200/50 dark:divide-slate-700/50">
<div class="flex-1 p-4 flex flex-col justify-center items-center hover:bg-slate-50/50 dark:hover:bg-slate-800/80 transition-colors">
<span class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-1 text-center">Total Penerima</span>
<span class="text-3xl font-extrabold bg-clip-text text-transparent bg-gradient-to-r from-indigo-600 to-purple-600 dark:from-indigo-400 dark:to-purple-400">{{ $total_penerima }}</span>
</div>
<div class="flex-1 p-4 flex flex-col justify-center items-center bg-gradient-to-b from-emerald-50/50 to-emerald-100/50 dark:from-emerald-900/10 dark:to-emerald-800/10 hover:from-emerald-100/50 hover:to-emerald-200/50 transition-colors">
<span class="text-xs font-semibold text-emerald-600 dark:text-emerald-400 uppercase tracking-widest mb-1 text-center">Sudah Cair</span>
<span class="text-3xl font-extrabold text-emerald-600 dark:text-emerald-400">{{ $sudah_cair }}</span>
</div>
<div class="flex-1 p-4 flex flex-col justify-center items-center bg-gradient-to-b from-rose-50/50 to-rose-100/50 dark:from-rose-900/10 dark:to-rose-800/10 hover:from-rose-100/50 hover:to-rose-200/50 transition-colors">
<span class="text-xs font-semibold text-rose-600 dark:text-rose-400 uppercase tracking-widest mb-1 text-center">Belum Cair</span>
<span class="text-3xl font-extrabold text-rose-600 dark:text-rose-400">{{ $belum_cair }}</span>
</div>
</div>
</div>
<!-- Add Form Container -->
<div id="form-container" class="hidden bg-white/60 dark:bg-slate-800/60 backdrop-blur-xl rounded-3xl border border-slate-200 dark:border-slate-700 p-6 md:p-8 mb-8 shadow-xl transition-all duration-500 transform origin-top">
<div class="flex items-center mb-6 border-b border-slate-200 dark:border-slate-700 pb-4">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white shadow-lg shadow-indigo-500/30 mr-4 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.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
</div>
<h3 class="text-2xl font-bold font-outfit text-slate-900 dark:text-white tracking-tight">Lengkapi Data Lokasi</h3>
</div>
<!-- Extra Fields for Ibadah -->
<div id="fields-ibadah" style="display:none; margin-top:10px; gap: 10px; flex-wrap: wrap;">
<select id="jenis_tempat_ibadah" style="padding: 10px; border-radius: 6px; border: 1px solid #ccc; color: black;">
<option value="">- Pilih Jenis Tempat Ibadah -</option>
<option value="Masjid">Masjid</option>
<option value="Gereja Katolik">Gereja Katolik</option>
<option value="Gereja Protestan">Gereja Protestan</option>
<option value="Vihara">Vihara</option>
<option value="Pura">Pura</option>
<option value="Klenteng">Klenteng</option>
</select>
<input type="text" id="kontak_person" placeholder="Kontak Person..." style="padding: 10px; border-radius: 6px; border: 1px solid #ccc; color: black;">
<input type="hidden" id="mode" value="{{ Auth::user()->role === 'admin' ? 'ibadah' : 'penerima' }}">
<input type="hidden" id="id_laporan_warga" value="{{ $laporan_fill ? $laporan_fill->id : '' }}">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Extra Fields for Ibadah -->
<div id="fields-ibadah" class="col-span-1 md:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-6" style="display:none;">
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Nama Tempat Ibadah <span class="text-rose-500">*</span></label>
<input type="text" id="nama_ibadah" class="form-input" placeholder="Contoh: Masjid Al-Ikhlas">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Jenis Tempat Ibadah <span class="text-rose-500">*</span></label>
<select id="jenis_tempat_ibadah" class="form-input">
<option value="">- Pilih Jenis Tempat Ibadah -</option>
<option value="Masjid">Masjid</option>
<option value="Gereja Katolik">Gereja Katolik</option>
<option value="Gereja Protestan">Gereja Protestan</option>
<option value="Vihara">Vihara</option>
<option value="Pura">Pura</option>
<option value="Klenteng">Klenteng</option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Nama Pengurus</label>
<input type="text" id="nama_pengurus" class="form-input" placeholder="Masukkan nama pengurus">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Kontak Pengurus</label>
<input type="text" id="kontak_pengurus" class="form-input" placeholder="Nomor telepon/HP">
</div>
</div>
<!-- Extra Fields for Penerima -->
<div id="fields-penerima" class="col-span-1 md:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-6" style="display:none;">
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Nama Kepala Keluarga <span class="text-rose-500">*</span></label>
<input type="text" id="nama_kepala_keluarga" class="form-input" placeholder="Masukkan nama kepala keluarga" value="{{ $laporan_fill ? $laporan_fill->nama_calon : '' }}" required>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">NIK Kepala Keluarga <span class="text-rose-500">*</span></label>
<input type="text" id="nik_kepala_keluarga" class="form-input" placeholder="16 digit NIK" maxlength="16" required>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Nomor Kartu Keluarga <span class="text-rose-500">*</span></label>
<input type="text" id="nomor_kk" class="form-input" placeholder="16 digit Nomor KK" maxlength="16" required>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Jumlah Tanggungan <span class="text-rose-500">*</span></label>
<input type="number" id="jumlah_tanggungan" class="form-input" placeholder="Jumlah anggota keluarga" min="0" value="{{ $laporan_fill ? $laporan_fill->jumlah_tanggungan : '' }}" required>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">PDF Form Investigasi <span class="text-rose-500">*</span></label>
<input type="file" id="dokumen_laporan" accept=".pdf" class="form-input !p-1.5" required>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Foto Rumah <span class="text-slate-400 font-normal">(Opsional)</span></label>
<input type="file" id="foto" accept="image/*" multiple class="form-input !p-1.5">
</div>
</div>
</div>
<!-- Extra Fields for Penerima -->
<div id="fields-penerima" style="display:none; margin-top:10px; gap: 10px; flex-wrap: wrap;">
<input type="text" id="nik_kepala_keluarga" placeholder="NIK Kepala Keluarga..." style="padding: 10px; border-radius: 6px; border: 1px solid #ccc; color: black;">
<input type="text" id="nomor_kk" placeholder="Nomor KK..." style="padding: 10px; border-radius: 6px; border: 1px solid #ccc; color: black;">
<input type="number" id="jumlah_tanggungan" placeholder="Jml Tanggungan..." style="padding: 10px; border-radius: 6px; border: 1px solid #ccc; color: black; width: 150px;" min="0">
<select id="id_tempat_ibadah" style="padding: 10px; border-radius: 6px; border: 1px solid #ccc; color: black;">
<option value="">- Pilih Tempat Ibadah -</option>
@foreach($ibadahs as $ib)
<option value="{{ $ib->id }}">{{ $ib->nama_tempat }}</option>
@endforeach
</select>
<label style="display:flex; align-items:center; gap:5px; color:#333; font-weight:bold;">
Foto Rumah: <input type="file" id="foto_kondisi_rumah" accept="image/*" style="color: black;">
</label>
<div class="mt-8 p-4 bg-indigo-50/50 dark:bg-indigo-900/20 backdrop-blur-sm rounded-xl border border-indigo-100 dark:border-indigo-800/50 flex items-start">
<div class="w-8 h-8 rounded-full bg-indigo-100 dark:bg-indigo-800 flex items-center justify-center shrink-0 mr-3 mt-0.5">
<svg class="w-5 h-5 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<p class="text-sm text-indigo-800 dark:text-indigo-200 mt-1.5">
Setelah melengkapi formulir di atas, <strong>klik pada titik lokasi yang sesuai di peta</strong> di bawah untuk menyimpan data.
</p>
</div>
<small style="color: #666; display: block; margin-top: 10px; font-style: italic;">👉 Pilih jenis titik, isi kolom yang tersedia, lalu <b>klik pada peta</b> untuk menyimpan lokasinya.</small>
</div>
<script>
function setMode(mode) {
var currentMode = document.getElementById('mode').value;
document.addEventListener('DOMContentLoaded', function() {
@if(isset($laporan_fill))
document.getElementById('btn-toggle-form').click();
alert('Data laporan diisi otomatis. Silakan klik pada peta untuk menentukan lokasi rumah penerima bantuan, lengkapi data lainnya, lalu klik Simpan.');
@endif
});
document.getElementById('btn-toggle-form').addEventListener('click', function() {
var formContainer = document.getElementById('form-container');
var mapEl = document.getElementById('map');
var mode = document.getElementById('mode').value;
var btnText = document.getElementById('btn-text');
var btnIconPlus = document.getElementById('btn-icon-plus');
var btnIconMinus = document.getElementById('btn-icon-minus');
// Toggle off if clicking the same mode
if (currentMode === mode) {
document.getElementById('mode').value = '';
document.getElementById('btn-mode-ibadah').style.background = '#e2e8f0';
document.getElementById('btn-mode-ibadah').style.color = '#333';
document.getElementById('btn-mode-penerima').style.background = '#e2e8f0';
document.getElementById('btn-mode-penerima').style.color = '#333';
if (formContainer.classList.contains('hidden')) {
formContainer.classList.remove('hidden');
btnText.innerText = 'Batal Tambah Data';
this.classList.replace('bg-indigo-600', 'bg-red-500');
this.classList.replace('hover:bg-indigo-700', 'hover:bg-red-600');
this.classList.replace('focus:ring-indigo-500', 'focus:ring-red-500');
btnIconPlus.classList.add('hidden');
btnIconMinus.classList.remove('hidden');
if(mode === 'ibadah') {
document.getElementById('fields-ibadah').style.display = 'grid';
document.getElementById('fields-penerima').style.display = 'none';
} else {
document.getElementById('fields-ibadah').style.display = 'none';
document.getElementById('fields-penerima').style.display = 'grid';
}
mapEl.classList.add('mode-tambah');
} else {
formContainer.classList.add('hidden');
btnText.innerText = 'Tambah Data Baru';
this.classList.replace('bg-red-500', 'bg-indigo-600');
this.classList.replace('hover:bg-red-600', 'hover:bg-indigo-700');
this.classList.replace('focus:ring-red-500', 'focus:ring-indigo-500');
btnIconMinus.classList.add('hidden');
btnIconPlus.classList.remove('hidden');
document.getElementById('fields-ibadah').style.display = 'none';
document.getElementById('fields-penerima').style.display = 'none';
mapEl.classList.remove('mode-tambah');
return;
}
document.getElementById('mode').value = mode;
mapEl.classList.add('mode-tambah');
if(mode === 'ibadah') {
document.getElementById('btn-mode-ibadah').style.background = '#007bff';
document.getElementById('btn-mode-ibadah').style.color = 'white';
document.getElementById('btn-mode-penerima').style.background = '#e2e8f0';
document.getElementById('btn-mode-penerima').style.color = '#333';
document.getElementById('fields-ibadah').style.display = 'flex';
document.getElementById('fields-penerima').style.display = 'none';
} else {
document.getElementById('btn-mode-penerima').style.background = '#007bff';
document.getElementById('btn-mode-penerima').style.color = 'white';
document.getElementById('btn-mode-ibadah').style.background = '#e2e8f0';
document.getElementById('btn-mode-ibadah').style.color = '#333';
document.getElementById('fields-ibadah').style.display = 'none';
document.getElementById('fields-penerima').style.display = 'flex';
}
}
});
</script>
<div id="panel-edit" class="edit-form">
<form action="{{ route('map.edit') }}" method="POST">
<!-- Edit Data Panel (Map specific) -->
<div id="panel-edit" class="hidden bg-amber-50/80 dark:bg-amber-900/20 backdrop-blur-md border border-amber-200/50 dark:border-amber-700/50 rounded-3xl p-6 md:p-8 mb-8 shadow-lg transition-all duration-300">
<div class="flex items-center mb-6 border-b border-amber-200/50 dark:border-amber-700/50 pb-4">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center text-white shadow-lg shadow-amber-500/30 mr-4 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="M11 5H6a2 2 0 00-2-2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</div>
<h3 class="text-2xl font-bold font-outfit text-slate-900 dark:text-white tracking-tight">Edit Data Lokasi Peta</h3>
</div>
<form action="{{ route('map.edit') }}" method="POST" class="grid grid-cols-1 md:grid-cols-2 gap-6">
@csrf
<strong>Edit Data:</strong>
<input type="hidden" name="id" id="edit-id">
<input type="hidden" name="type" id="edit-type">
<input type="text" name="nama" id="edit-nama" placeholder="Nama" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc; color: black;">
<input type="text" name="alamat" id="edit-alamat" placeholder="Alamat" style="padding: 8px; width: 400px; border-radius: 4px; border: 1px solid #ccc; color: black;">
<button type="submit" style="padding: 8px 15px; background: #28a745; color: white; border: none; border-radius: 4px;">Simpan</button>
<button type="button" onclick="closeEdit()" style="padding: 8px 15px; background: #6c757d; color: white; border: none; border-radius: 4px;">Batal</button>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Nama</label>
<input type="text" name="nama" id="edit-nama" class="form-input" placeholder="Nama">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Alamat Lengkap</label>
<input type="text" name="alamat" id="edit-alamat" class="form-input" placeholder="Alamat">
</div>
<div class="col-span-1 md:col-span-2 flex justify-end space-x-4 mt-4">
<button type="button" onclick="closeEdit()" class="px-6 py-2.5 bg-white/80 dark:bg-slate-700/80 backdrop-blur-sm border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-200 rounded-full hover:bg-slate-50 dark:hover:bg-slate-600 font-medium transition-all shadow-sm">Batal</button>
<button type="submit" class="px-6 py-2.5 bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-400 hover:to-orange-400 text-white rounded-full font-medium shadow-lg shadow-amber-500/30 hover:-translate-y-0.5 transition-all">Simpan Perubahan</button>
</div>
</form>
</div>
<div id="map"></div>
<!-- The Map Layout -->
<div class="{{ Auth::user()->role === 'user' ? 'flex flex-col lg:flex-row gap-6 items-stretch' : '' }}">
<!-- Map Column -->
<div class="{{ Auth::user()->role === 'user' ? 'flex-grow lg:w-2/3 flex flex-col' : '' }}">
<div class="rounded-3xl overflow-hidden shadow-xl shadow-slate-200/50 dark:shadow-slate-900/50 border border-slate-200/80 dark:border-slate-700/80 relative {{ Auth::user()->role === 'user' ? 'flex-grow min-h-[500px]' : '' }} bg-slate-100 dark:bg-slate-800 z-10">
<div id="map" class="{{ Auth::user()->role === 'user' ? 'absolute inset-0 w-full h-full' : 'w-full' }}" style="{{ Auth::user()->role !== 'user' ? 'height: 550px;' : '' }}"></div>
</div>
</div>
<!-- Info Column (User Only) -->
@if(Auth::user()->role === 'user')
<div class="w-full lg:w-1/3 flex flex-col">
<div class="bg-white/60 dark:bg-slate-800/60 backdrop-blur-xl border border-slate-200/80 dark:border-slate-700/80 rounded-3xl shadow-xl shadow-slate-200/50 dark:shadow-slate-900/50 overflow-hidden flex flex-col h-full min-h-[500px] max-h-[500px]">
<div class="bg-indigo-50/80 dark:bg-indigo-900/40 backdrop-blur-sm px-6 py-5 border-b border-indigo-100 dark:border-indigo-800/50 flex justify-between items-center shrink-0">
<h3 class="font-bold font-outfit text-xl text-slate-900 dark:text-white tracking-tight truncate">Tempat Ibadah Anda</h3>
</div>
<div class="p-6 flex-grow overflow-y-auto">
@forelse($ibadahs as $index => $ib)
<div class="mb-6 last:mb-0 bg-white dark:bg-slate-700/50 p-5 rounded-2xl border border-slate-100 dark:border-slate-600 shadow-sm">
<h4 class="font-bold text-lg text-slate-900 dark:text-white mb-2">{{ $ib->nama_tempat }}</h4>
<span class="inline-block px-3 py-1.5 bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300 text-xs font-bold rounded-full mb-4">{{ $ib->jenis_tempat_ibadah ?? 'Lainnya' }}</span>
<div class="space-y-3 text-sm text-slate-600 dark:text-slate-300">
<div class="flex justify-between border-b border-slate-100 dark:border-slate-600/50 pb-1.5">
<span class="text-slate-500 font-medium">Pengurus</span>
<span class="text-slate-900 dark:text-slate-200">{{ $ib->nama_pengurus ?? '-' }}</span>
</div>
<div class="flex justify-between border-b border-slate-100 dark:border-slate-600/50 pb-1.5">
<span class="text-slate-500 font-medium">Kontak</span>
<span class="text-slate-900 dark:text-slate-200">{{ $ib->kontak_pengurus ?? '-' }}</span>
</div>
<div class="flex justify-between border-b border-slate-100 dark:border-slate-600/50 pb-1.5">
<span class="text-slate-500 font-medium">Radius</span>
<span class="text-slate-900 dark:text-slate-200 font-bold text-indigo-600 dark:text-indigo-400">{{ $ib->radius_meter }} m</span>
</div>
<div class="flex flex-col pt-1.5">
<span class="text-slate-500 font-medium mb-1">Alamat</span>
<span class="text-slate-900 dark:text-slate-200 leading-relaxed">{{ $ib->alamat }}</span>
</div>
</div>
<div class="mt-5 pt-4 border-t border-slate-100 dark:border-slate-600">
<button onclick='openEditIbadah({{ $ib->id }}, "{{ addslashes($ib->nama_tempat) }}", "{{ addslashes($ib->alamat) }}", {{ $ib->radius_meter }}, "{{ addslashes($ib->jenis_tempat_ibadah) }}", "{{ addslashes($ib->nama_pengurus) }}", "{{ addslashes($ib->kontak_pengurus) }}")' class="w-full inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-semibold rounded-xl shadow-sm text-amber-800 bg-amber-100/80 hover:bg-amber-200 focus:outline-none transition-all duration-300">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
Edit Data Lokasi
</button>
</div>
</div>
@empty
<div class="flex flex-col items-center justify-center py-12 px-4 text-center">
<div class="w-16 h-16 bg-slate-100 dark:bg-slate-700 rounded-full flex items-center justify-center mb-4">
<svg class="w-8 h-8 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4M12 20V4"></path></svg>
</div>
<h4 class="text-lg font-bold text-slate-900 dark:text-white mb-1">Belum ada data</h4>
<p class="text-sm text-slate-500 dark:text-slate-400">Anda belum menambahkan lokasi tempat ibadah.</p>
</div>
@endforelse
</div>
</div>
</div>
@endif
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
var map = L.map('map').setView([-0.02, 109.34], 14);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
attribution: '&copy; OpenStreetMap contributors &copy; CARTO',
subdomains: 'abcd',
maxZoom: 20
}).addTo(map);
var listIbadah = [];
var listPenerima = [];
var iconIbadah = L.divIcon({
className: '',
html: `<div style="background:#007bff; color:#fff; border-radius:50%; width:30px; height:30px; display:flex; align-items:center; justify-content:center; border:2px solid #fff;">🏛️</div>`,
iconSize: [30, 30]
className: 'custom-div-icon',
html: `<div class="shadow-lg" style="background:#4f46e5; color:#fff; border-radius:50%; width:36px; height:36px; display:flex; align-items:center; justify-content:center; border:3px solid #fff; font-size:16px;">🏛️</div>`,
iconSize: [36, 36],
iconAnchor: [18, 18]
});
// 1. Fungsi Edit UI
function openEdit(id, type, nama, alamat) {
document.getElementById('panel-edit').style.display = 'block';
var panel = document.getElementById('panel-edit');
panel.classList.remove('hidden');
document.getElementById('edit-id').value = id;
document.getElementById('edit-type').value = type;
document.getElementById('edit-nama').value = nama;
document.getElementById('edit-alamat').value = alamat;
window.scrollTo(0,0);
// Scroll smoothly to edit panel
panel.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function closeEdit() { document.getElementById('panel-edit').style.display = 'none'; }
function closeEdit() { document.getElementById('panel-edit').classList.add('hidden'); }
// 2. Reverse Geocoding & Save
map.on('click', async function(e) {
const mode = document.getElementById('mode').value;
if(!mode) return; // Mode biasa, jangan lakukan apa-apa
if(document.getElementById('form-container').classList.contains('hidden')) return;
const nama = document.getElementById('nama').value;
if(!nama) return alert("Isi nama dulu!");
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${e.latlng.lat}&lon=${e.latlng.lng}`);
const data = await res.json();
const addr = data.address || {};
const alamatDetail = [addr.road, addr.village || addr.suburb, addr.city].filter(Boolean).join(", ");
let fd = new FormData();
fd.append('_token', '{{ csrf_token() }}');
fd.append('type', mode);
fd.append('nama', nama);
fd.append('lat', e.latlng.lat);
fd.append('lng', e.latlng.lng);
fd.append('alamat', alamatDetail || data.display_name);
let nama = '';
if (mode === 'ibadah') {
fd.append('jenis_tempat_ibadah', document.getElementById('jenis_tempat_ibadah').value);
fd.append('kontak_person', document.getElementById('kontak_person').value);
nama = document.getElementById('nama_ibadah').value;
if(!nama) return alert("Isi nama tempat ibadah dulu!");
if(!document.getElementById('jenis_tempat_ibadah').value) return alert("Pilih jenis tempat ibadah!");
} else {
fd.append('nik_kepala_keluarga', document.getElementById('nik_kepala_keluarga').value);
fd.append('nomor_kk', document.getElementById('nomor_kk').value);
fd.append('jumlah_tanggungan', document.getElementById('jumlah_tanggungan').value);
fd.append('id_tempat_ibadah', document.getElementById('id_tempat_ibadah').value);
nama = document.getElementById('nama_kepala_keluarga').value;
if(!nama) return alert("Isi nama kepala keluarga (wajib)!");
const fotoInput = document.getElementById('foto_kondisi_rumah');
if (fotoInput.files.length > 0) {
fd.append('foto_kondisi_rumah', fotoInput.files[0]);
}
const nik = document.getElementById('nik_kepala_keluarga').value;
if(!nik) return alert("Isi NIK Kepala Keluarga (wajib)!");
const kk = document.getElementById('nomor_kk').value;
if(!kk) return alert("Isi Nomor KK (wajib)!");
const tgg = document.getElementById('jumlah_tanggungan').value;
if(!tgg) return alert("Isi Jumlah Tanggungan (wajib)!");
const docInput = document.getElementById('dokumen_laporan');
if(docInput.files.length === 0) return alert("PDF Form Investigasi wajib diunggah!");
}
fetch('{{ route("map.save") }}', { method: 'POST', body: fd }).then(() => location.reload());
// Loading indicator
document.body.style.cursor = 'wait';
try {
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${e.latlng.lat}&lon=${e.latlng.lng}`);
const data = await res.json();
const addr = data.address || {};
const alamatDetail = [addr.road, addr.village || addr.suburb, addr.city].filter(Boolean).join(", ");
let fd = new FormData();
fd.append('_token', '{{ csrf_token() }}');
fd.append('type', mode);
fd.append('nama', nama);
fd.append('lat', e.latlng.lat);
fd.append('lng', e.latlng.lng);
fd.append('alamat', alamatDetail || data.display_name);
let idLaporan = document.getElementById('id_laporan_warga').value;
if (idLaporan) {
fd.append('id_laporan_warga', idLaporan);
}
if (mode === 'ibadah') {
fd.append('jenis_tempat_ibadah', document.getElementById('jenis_tempat_ibadah').value);
fd.append('nama_pengurus', document.getElementById('nama_pengurus').value);
fd.append('kontak_pengurus', document.getElementById('kontak_pengurus').value);
} else {
fd.append('nik_kepala_keluarga', document.getElementById('nik_kepala_keluarga').value);
fd.append('nomor_kk', document.getElementById('nomor_kk').value);
fd.append('jumlah_tanggungan', document.getElementById('jumlah_tanggungan').value);
const docInput = document.getElementById('dokumen_laporan');
if (docInput.files.length > 0) {
fd.append('dokumen_laporan', docInput.files[0]);
}
const fotoInput = document.getElementById('foto');
if (fotoInput.files.length > 0) {
for (let i = 0; i < fotoInput.files.length; i++) {
fd.append('foto[]', fotoInput.files[i]);
}
}
}
const saveRes = await fetch('{{ route("map.save") }}', { method: 'POST', body: fd });
const saveData = await saveRes.json();
if (saveData.status === 'error') {
alert(saveData.message);
document.body.style.cursor = 'default';
return;
}
location.reload();
} catch (err) {
alert("Terjadi kesalahan saat menyimpan data lokasi.");
document.body.style.cursor = 'default';
}
});
// 3. Load Data Ibadah
@@ -202,25 +414,31 @@
(function(){
var rAwal = {{ $ib->radius_meter }};
var circle = L.circle([{{ $ib->lat }}, {{ $ib->lng }}], {
radius: rAwal, color: 'blue', fillOpacity: 0.1
radius: rAwal, color: '#4f46e5', fillColor: '#4f46e5', fillOpacity: 0.1, weight: 2, dashArray: '5, 5'
}).addTo(map);
var marker = L.marker([{{ $ib->lat }}, {{ $ib->lng }}], {icon: iconIbadah}).addTo(map);
listIbadah.push({id: {{ $ib->id }}, nama: "{{ addslashes($ib->nama_tempat) }}", circle: circle, latlng: L.latLng({{ $ib->lat }}, {{ $ib->lng }})});
marker.bindPopup(`
<b>{{ addslashes($ib->nama_tempat) }}</b><br>
Radius: <span id="val_{{ $ib->id }}">${rAwal}</span> m<br>
<div class="text-center pb-2">
<strong class="text-gray-800 text-base block mb-1">{{ addslashes($ib->nama_tempat) }}</strong>
<span class="text-xs bg-indigo-100 text-indigo-800 px-2 py-1 rounded-full">Radius: <span id="val_{{ $ib->id }}">${rAwal}</span> m</span>
</div>
@if(Auth::user()->role !== 'user')
<input type="range" class="popup-slider" min="100" max="3000" value="${rAwal}" oninput="updateRadius(${rAwal}, this.value, {{ $ib->id }})" onchange="saveRadius(this.value, {{ $ib->id }})">
<button onclick="window.location.href='{{ route('map.delete', ['type' => 'ibadah', 'id' => $ib->id]) }}'" style="background:red; color:white; border:none; padding:5px; width:100%; cursor:pointer; margin-top:5px;">Hapus</button>
@endif
`);
})();
@endforeach
// 4. Load Data Penerima Bantuan
@foreach($penerimas as $r)
@if($r->status_persetujuan !== 'ditolak')
var rm = L.circleMarker([{{ $r->lat }}, {{ $r->lng }}], {radius: 8, color: '#fff', fillOpacity: 1, weight: 2}).addTo(map);
listPenerima.push({marker: rm, latlng: L.latLng({{ $r->lat }}, {{ $r->lng }})});
rm.bindPopup(`<strong class="text-gray-800">{{ addslashes($r->nama_kepala_keluarga) }}</strong>`);
listPenerima.push({marker: rm, latlng: L.latLng({{ $r->lat }}, {{ $r->lng }}), statusPersetujuan: "{{ $r->status_persetujuan }}"});
@endif
@endforeach
// 5. Real-time Update Logic
@@ -239,21 +457,15 @@
}
function syncUI() {
var baris = document.querySelectorAll('.baris-penerima');
baris.forEach(row => {
var rLoc = L.latLng(row.getAttribute('data-lat'), row.getAttribute('data-lng'));
var covered = [];
listIbadah.forEach(ib => { if(rLoc.distanceTo(ib.latlng) <= ib.circle.getRadius()) covered.push(ib.nama); });
row.querySelector('.status-cell').innerHTML = covered.length > 0 ?
`<span style='color:green'>✔ ${covered.join(", ")}</span>` :
`<span style='color:red'>✘ Luar Jangkauan</span>`;
});
listPenerima.forEach(penerima => {
let check = false;
listIbadah.forEach(ib => { if(penerima.latlng.distanceTo(ib.latlng) <= ib.circle.getRadius()) check = true; });
penerima.marker.setStyle({fillColor: check ? 'green' : 'red'});
let fillColor = check ? '#10b981' : '#ef4444'; // emerald-500 : red-500
if (penerima.statusPersetujuan !== 'disetujui') {
fillColor = '#f59e0b'; // amber-500
}
penerima.marker.setStyle({fillColor: fillColor});
});
}
@@ -261,6 +473,73 @@
</script>
<!-- Map Interface End -->
@if(Auth::user()->role === 'user')
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
<!-- Edit Ibadah Form Modal/Panel -->
<div id="panel-edit-ibadah" class="hidden mt-8 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-2xl shadow-lg p-6">
<div class="flex items-center mb-5 border-b border-gray-100 dark:border-gray-700 pb-4">
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Edit Data Tempat Ibadah</h3>
</div>
<form action="{{ route('map.edit') }}" method="POST">
@csrf
<input type="hidden" name="id" id="edit-ibadah-id">
<input type="hidden" name="type" id="edit-ibadah-type" value="ibadah">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-6">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Nama Tempat</label>
<input type="text" name="nama" id="edit-ibadah-nama" class="form-input" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Jenis Tempat Ibadah</label>
<select name="jenis_tempat_ibadah" id="edit-ibadah-jenis" class="form-input">
<option value="">- Pilih Jenis -</option>
<option value="Masjid">Masjid</option>
<option value="Gereja Katolik">Gereja Katolik</option>
<option value="Gereja Protestan">Gereja Protestan</option>
<option value="Vihara">Vihara</option>
<option value="Pura">Pura</option>
<option value="Klenteng">Klenteng</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Nama Pengurus</label>
<input type="text" name="nama_pengurus" id="edit-ibadah-nama-pengurus" class="form-input">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Kontak Pengurus</label>
<input type="text" name="kontak_pengurus" id="edit-ibadah-kontak-pengurus" class="form-input">
</div>
<div class="col-span-1 md:col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Alamat Lengkap</label>
<input type="text" name="alamat" id="edit-ibadah-alamat" class="form-input" required>
</div>
</div>
<div class="flex justify-end space-x-3">
<button type="button" onclick="closeEditIbadah()" class="px-5 py-2.5 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-600 font-medium transition-colors">Batal</button>
<button type="submit" class="px-5 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl font-medium shadow-sm transition-colors">Simpan Perubahan</button>
</div>
</form>
</div>
<script>
function openEditIbadah(id, nama, alamat, radius, jenis, nama_pengurus, kontak_pengurus) {
var panel = document.getElementById('panel-edit-ibadah');
panel.classList.remove('hidden');
document.getElementById('edit-ibadah-id').value = id;
document.getElementById('edit-ibadah-nama').value = nama;
document.getElementById('edit-ibadah-alamat').value = alamat;
document.getElementById('edit-ibadah-jenis').value = jenis || '';
document.getElementById('edit-ibadah-nama-pengurus').value = nama_pengurus || '';
document.getElementById('edit-ibadah-kontak-pengurus').value = kontak_pengurus || '';
panel.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function closeEditIbadah() { document.getElementById('panel-edit-ibadah').classList.add('hidden'); }
</script>
</div>
@endif
</div>
</div>
</div>
+63
View File
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Konfirmasi Penyerahan Bantuan</title>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #f4f7f6; margin: 0; padding: 20px; color: #333; }
.container { max-width: 500px; margin: 0 auto; background: white; padding: 30px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
h2 { text-align: center; color: #2c3e50; margin-top: 0; }
.info-box { background: #eef2f5; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
.info-box p { margin: 5px 0; }
.info-box strong { color: #2c3e50; }
.form-group { margin-bottom: 15px; }
label { display: block; font-weight: bold; margin-bottom: 5px; color: #555; }
input[type="password"], input[type="file"] { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; }
button { width: 100%; background: #28a745; color: white; border: none; padding: 12px; font-size: 16px; font-weight: bold; border-radius: 6px; cursor: pointer; transition: 0.3s; }
button:hover { background: #218838; }
.alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: bold; text-align: center; }
.alert-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.badge-success { background: #28a745; color: white; padding: 5px 10px; border-radius: 20px; font-size: 14px; font-weight: bold; display: inline-block; margin-top: 10px; }
</style>
</head>
<body>
<div class="container">
<h2>Konfirmasi Penyerahan Bantuan</h2>
@if(session('error'))
<div class="alert alert-error">{{ session('error') }}</div>
@endif
@if(isset($pesan_sukses))
<div class="alert alert-success">Berhasil! Bukti penyerahan telah disimpan dan menunggu persetujuan Admin.</div>
@endif
<div class="info-box">
<p><strong>Penerima:</strong> {{ $warga->nama_kepala_keluarga }}</p>
<p><strong>NIK:</strong> {{ substr($warga->nik_kepala_keluarga ?? '', 0, 6) }}**********</p>
<p><strong>Alamat:</strong> {{ $warga->alamat }}</p>
</div>
@if($sudah_cair)
<div style="text-align: center; margin-bottom: 20px;">
<div class="badge-success"> Sudah Dicairkan Bulan Ini</div>
<p style="color: #666; font-size: 14px; margin-top: 10px;">Bantuan untuk penerima ini telah diproses pada <strong>{{ $log->tanggal_diterima->format('d F Y H:i') }}</strong>.</p>
</div>
@else
<form id="konfirmasiForm" action="{{ route('konfirmasi.process', ['id' => $warga->id, 'signature' => request('signature')]) }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="form-group">
<label for="foto_penyerahan">Unggah Bukti Foto Penyerahan</label>
<input type="file" id="foto_penyerahan" name="foto_penyerahan" accept="image/*" required>
</div>
<button type="submit" id="btnSubmit">Konfirmasi Pencairan</button>
</form>
@endif
</div>
</body>
</html>
+268
View File
@@ -0,0 +1,268 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cetak Kupon Bantuan</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--primary: #2563eb;
--primary-dark: #1d4ed8;
--secondary: #f8fafc;
--text-main: #0f172a;
--text-muted: #64748b;
--border-color: #e2e8f0;
}
body {
font-family: 'Inter', sans-serif;
background-color: #f1f5f9;
margin: 0;
padding: 40px 20px;
color: var(--text-main);
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.controls {
text-align: center;
margin-bottom: 40px;
}
.btn-print {
background-color: var(--primary);
color: white;
padding: 14px 28px;
border-radius: 10px;
text-decoration: none;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s ease;
font-size: 16px;
display: inline-flex;
align-items: center;
gap: 10px;
box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.2);
}
.btn-print:hover {
background-color: var(--primary-dark);
transform: translateY(-1px);
box-shadow: 0 6px 8px -1px rgba(37, 99, 235, 0.3);
}
.print-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 24px;
max-width: 1000px;
margin: 0 auto;
}
.coupon-card {
background: white;
border-radius: 16px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
display: flex;
overflow: hidden;
border: 1px solid var(--border-color);
position: relative;
page-break-inside: avoid;
}
.coupon-left {
flex: 1;
padding: 32px 24px;
border-right: 2px dashed #cbd5e1;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
}
/* Cutout circles */
.coupon-left::before, .coupon-left::after {
content: '';
position: absolute;
right: -11px;
width: 20px;
height: 20px;
background-color: #f1f5f9;
border-radius: 50%;
border: 1px solid var(--border-color);
z-index: 1;
}
.coupon-left::before {
top: -11px;
border-top-color: transparent;
border-left-color: transparent;
transform: rotate(45deg);
}
.coupon-left::after {
bottom: -11px;
border-bottom-color: transparent;
border-left-color: transparent;
transform: rotate(135deg);
}
.coupon-right {
width: 160px;
background-color: var(--secondary);
padding: 24px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.header-title {
text-transform: uppercase;
font-size: 11px;
letter-spacing: 1.5px;
font-weight: 700;
color: var(--primary);
margin-bottom: 8px;
}
.beneficiary-name {
font-size: 22px;
font-weight: 800;
margin: 0 0 4px 0;
color: var(--text-main);
line-height: 1.2;
}
.beneficiary-nik {
font-size: 14px;
color: var(--text-muted);
margin: 0 0 20px 0;
font-family: monospace;
background: #f8fafc;
padding: 4px 8px;
border-radius: 6px;
display: inline-block;
}
.period-badge {
display: inline-flex;
align-items: center;
padding: 6px 14px;
background-color: #eff6ff;
color: #1e3a8a;
border-radius: 20px;
font-weight: 600;
font-size: 13px;
margin-bottom: 16px;
border: 1px solid #bfdbfe;
}
.qr-wrapper {
background: white;
padding: 8px;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
border: 1px solid var(--border-color);
}
.qr-wrapper img {
display: block;
width: 110px;
height: 110px;
}
.qr-label {
font-size: 10px;
color: var(--text-muted);
margin-top: 12px;
text-align: center;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.footer-note {
font-size: 11px;
color: var(--text-muted);
margin-top: auto;
line-height: 1.5;
display: flex;
align-items: flex-start;
gap: 6px;
}
.footer-note svg {
width: 14px;
height: 14px;
flex-shrink: 0;
color: var(--primary);
}
@media print {
body {
background-color: white;
padding: 0;
}
.controls { display: none; }
.print-container {
display: block;
max-width: 100%;
}
.coupon-card {
box-shadow: none;
border: 2px solid #cbd5e1;
margin-bottom: 24px;
break-inside: avoid;
}
.coupon-left::before, .coupon-left::after {
background-color: white;
}
}
</style>
</head>
<body>
<div class="controls">
<button class="btn-print" onclick="window.print()">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"></path>
</svg>
Cetak Kupon Sekarang
</button>
</div>
<div class="print-container">
@foreach($kupons as $kupon)
<div class="coupon-card">
<div class="coupon-left">
<div class="header-title">Voucher Pencairan Bantuan</div>
<h2 class="beneficiary-name">{{ $kupon['warga']->nama_kepala_keluarga }}</h2>
<div class="beneficiary-nik">NIK: {{ substr($kupon['warga']->nik_kepala_keluarga ?? '', 0, 6) }}&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;</div>
<div>
<span class="period-badge">
<svg width="14" height="14" style="margin-right: 6px;" fill="none" stroke="currentColor" 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"></path></svg>
Periode: {{ $kupon['periode_text'] }}
</span>
</div>
<div class="footer-note">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
Tunjukkan QR Code ini kepada pengurus tempat penyalur untuk proses verifikasi.
</div>
</div>
<div class="coupon-right">
<div class="qr-wrapper">
<img src="{{ $kupon['qrDataUri'] }}" alt="QR Code Kupon">
</div>
<div class="qr-label">Scan untuk<br>Verifikasi</div>
</div>
</div>
@endforeach
</div>
</body>
</html>
+102
View File
@@ -0,0 +1,102 @@
<x-app-layout>
<x-slot name="header">
<div class="flex justify-between items-center">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Daftar Laporan Warga') }}
</h2>
</div>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
<div class="mb-8 p-4 bg-indigo-50 dark:bg-indigo-900/30 rounded-xl border border-indigo-100 dark:border-indigo-800 flex flex-col md:flex-row items-center justify-between gap-4">
<div>
<h3 class="text-lg font-bold text-indigo-900 dark:text-indigo-300">Tautan Pelaporan Warga</h3>
<p class="text-sm text-indigo-700 dark:text-indigo-400 mt-1">Gunakan tautan ini atau jadikan QR Code agar warga dapat melapor langsung ke wilayah Anda.</p>
</div>
<div class="flex w-full md:w-auto items-center gap-2">
<input type="text" readonly value="{{ url('/lapor/' . $tempatIbadah->kode_lapor) }}" id="link-lapor" class="w-full md:w-80 bg-white dark:bg-gray-800 text-sm border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-gray-700 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500">
<button onclick="copyLink()" class="flex-shrink-0 bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Salin
</button>
</div>
</div>
<script>
function copyLink() {
var copyText = document.getElementById("link-lapor");
copyText.select();
copyText.setSelectionRange(0, 99999); // For mobile devices
navigator.clipboard.writeText(copyText.value);
alert("Tautan berhasil disalin: " + copyText.value);
}
</script>
@if($laporans->isEmpty())
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<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"></path>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">Tidak ada laporan baru</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Semua laporan warga telah diproses atau belum ada yang melaporkan.</p>
</div>
@else
<div class="overflow-x-auto">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">Tanggal Lapor</th>
<th scope="col" class="px-6 py-3">Nama Calon Penerima</th>
<th scope="col" class="px-6 py-3">Tanggungan</th>
<th scope="col" class="px-6 py-3">Alamat</th>
<th scope="col" class="px-6 py-3">Deskripsi</th>
<th scope="col" class="px-6 py-3">Foto Bukti</th>
<th scope="col" class="px-6 py-3">Aksi</th>
</tr>
</thead>
<tbody>
@foreach($laporans as $laporan)
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="px-6 py-4">{{ $laporan->created_at->format('d M Y H:i') }}</td>
<td class="px-6 py-4 font-medium text-gray-900 dark:text-white">{{ $laporan->nama_calon }}</td>
<td class="px-6 py-4">{{ $laporan->jumlah_tanggungan }} Orang</td>
<td class="px-6 py-4">
@if($laporan->alamat)
<span class="text-gray-700 dark:text-gray-300">{{ $laporan->alamat }}</span>
@else
<span class="text-gray-400 italic">Tidak ada alamat</span>
@endif
</td>
<td class="px-6 py-4">
@if($laporan->deskripsi)
<span class="text-gray-700 dark:text-gray-300">{{ Str::limit($laporan->deskripsi, 50) }}</span>
@else
<span class="text-gray-400 italic">Tidak ada deskripsi</span>
@endif
</td>
<td class="px-6 py-4">
@if($laporan->foto)
<a href="{{ asset('storage/uploads/' . $laporan->foto) }}" target="_blank" class="text-blue-600 hover:underline">Lihat Foto</a>
@else
<span class="text-gray-400 italic">Tidak ada foto</span>
@endif
</td>
<td class="px-6 py-4">
<a href="{{ route('dashboard', ['add_laporan' => $laporan->id]) }}" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition">
Tambah Data Baru dari Laporan Ini
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
</div>
</div>
</div>
</x-app-layout>
+120
View File
@@ -0,0 +1,120 @@
<x-guest-layout>
<div class="mb-8 text-center">
<div class="mx-auto w-16 h-16 bg-indigo-100 dark:bg-indigo-900/50 rounded-full flex items-center justify-center mb-4 border-4 border-white dark:border-gray-800 shadow-sm">
<svg class="w-8 h-8 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></path></svg>
</div>
<h2 class="text-2xl font-extrabold text-gray-900 dark:text-white tracking-tight">Laporan Warga</h2>
<p class="text-sm font-medium text-indigo-600 dark:text-indigo-400 mt-1">Untuk: {{ $tempatIbadah->nama_tempat }}</p>
<p class="text-gray-500 dark:text-gray-400 mt-2 text-sm max-w-sm mx-auto">
Bantu kami menemukan warga sekitar yang membutuhkan. Masukkan data calon penerima bantuan di bawah ini.
</p>
</div>
@if (session('success'))
<div class="mb-6 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 text-green-700 dark:text-green-400 p-4 rounded-xl flex items-start shadow-sm" role="alert">
<svg class="w-5 h-5 mr-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<div class="text-sm font-medium">
{{ session('success') }}
</div>
</div>
@endif
<form method="POST" action="{{ route('lapor.store', $tempatIbadah->kode_lapor) }}" enctype="multipart/form-data" class="space-y-6">
@csrf
<!-- Nama Calon -->
<div class="relative">
<label for="nama_calon" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1">Nama Kepala Keluarga <span class="text-red-500">*</span></label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
</div>
<input id="nama_calon" type="text" name="nama_calon" value="{{ old('nama_calon') }}" required autofocus
class="pl-10 block w-full rounded-xl border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm transition-colors py-3"
placeholder="Contoh: Budi Santoso">
</div>
<x-input-error :messages="$errors->get('nama_calon')" class="mt-2" />
</div>
<!-- Jumlah Tanggungan -->
<div class="relative">
<label for="jumlah_tanggungan" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1">Jumlah Tanggungan <span class="text-red-500">*</span></label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
</div>
<input id="jumlah_tanggungan" type="number" name="jumlah_tanggungan" min="0" value="{{ old('jumlah_tanggungan') }}" required
class="pl-10 block w-full rounded-xl border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm transition-colors py-3"
placeholder="Jumlah anggota keluarga">
</div>
<x-input-error :messages="$errors->get('jumlah_tanggungan')" class="mt-2" />
</div>
<!-- Alamat -->
<div class="relative">
<label for="alamat" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1">Alamat Lengkap <span class="text-red-500">*</span></label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 pt-3 flex items-start pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
</div>
<textarea id="alamat" name="alamat" rows="2" required
class="pl-10 block w-full rounded-xl border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm transition-colors py-3 px-4"
placeholder="Masukkan alamat lengkap RT/RW, Dusun, Desa..."></textarea>
</div>
<x-input-error :messages="$errors->get('alamat')" class="mt-2" />
</div>
<!-- Deskripsi -->
<div class="relative">
<label for="deskripsi" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1">Deskripsi Kondisi Warga <span class="text-gray-400 font-normal">(Opsional)</span></label>
<div class="relative">
<textarea id="deskripsi" name="deskripsi" rows="3"
class="block w-full rounded-xl border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm transition-colors py-3 px-4"
placeholder="Ceritakan sedikit tentang kondisi ekonomi atau kehidupan warga ini..."></textarea>
</div>
<x-input-error :messages="$errors->get('deskripsi')" class="mt-2" />
</div>
<!-- Foto Upload (Custom) -->
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1">Foto Kondisi / Rumah <span class="text-gray-400 font-normal">(Opsional namun sangat disarankan)</span></label>
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 dark:border-gray-600 border-dashed rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors group relative cursor-pointer" onclick="document.getElementById('foto').click()">
<div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400 group-hover:text-indigo-500 transition-colors" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex text-sm text-gray-600 dark:text-gray-400 justify-center">
<span class="relative cursor-pointer bg-transparent rounded-md font-medium text-indigo-600 dark:text-indigo-400 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
<span>Upload foto</span>
<input id="foto" name="foto" type="file" class="sr-only" accept="image/*" onchange="updateFileName(this)">
</span>
<p class="pl-1">atau tarik & lepas ke sini</p>
</div>
<p class="text-xs text-gray-500 dark:text-gray-500">
PNG, JPG, JPEG maks 2MB
</p>
</div>
</div>
<p id="file-name" class="mt-2 text-sm text-indigo-600 dark:text-indigo-400 hidden font-medium text-center"></p>
<x-input-error :messages="$errors->get('foto')" class="mt-2" />
</div>
<div class="pt-4">
<button type="submit" class="w-full flex justify-center py-3.5 px-4 border border-transparent rounded-xl shadow-sm text-sm font-bold text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-all transform hover:-translate-y-0.5">
Kirim Laporan
</button>
</div>
</form>
<script>
function updateFileName(input) {
const fileNameDisplay = document.getElementById('file-name');
if (input.files && input.files.length > 0) {
fileNameDisplay.textContent = 'File terpilih: ' + input.files[0].name;
fileNameDisplay.classList.remove('hidden');
} else {
fileNameDisplay.classList.add('hidden');
}
}
</script>
</x-guest-layout>
+73 -9
View File
@@ -1,34 +1,98 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="scroll-smooth">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<title>{{ config('app.name', 'WebGIS Pengentasan Kemiskinan') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700,800|outfit:500,600,700,800" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
body {
font-family: 'Inter', sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Outfit', sans-serif;
}
.bg-grid-pattern {
background-image: linear-gradient(to right, rgba(0,0,0,0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(0,0,0,0.05) 1px, transparent 1px);
background-size: 40px 40px;
}
.dark .bg-grid-pattern {
background-image: linear-gradient(to right, rgba(255,255,255,0.03) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255,255,255,0.03) 1px, transparent 1px);
}
.blob-1 {
position: absolute;
top: -10%;
left: -10%;
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(99,102,241,0.15) 0%, rgba(transparent) 70%);
border-radius: 50%;
filter: blur(40px);
z-index: -1;
}
.blob-2 {
position: absolute;
bottom: -10%;
right: -10%;
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(168,85,247,0.15) 0%, rgba(transparent) 70%);
border-radius: 50%;
filter: blur(40px);
z-index: -1;
}
.dark .blob-1 { background: radial-gradient(circle, rgba(99,102,241,0.2) 0%, rgba(transparent) 70%); }
.dark .blob-2 { background: radial-gradient(circle, rgba(168,85,247,0.2) 0%, rgba(transparent) 70%); }
.animate-fade-in-up {
animation: fadeInUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
opacity: 0;
transform: translateY(20px);
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
@include('layouts.navigation')
<body class="antialiased bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 relative overflow-x-hidden selection:bg-indigo-500 selection:text-white">
<!-- Background Elements -->
<div class="fixed inset-0 bg-grid-pattern pointer-events-none z-[-2]"></div>
<div class="fixed inset-0 pointer-events-none z-[-1] overflow-hidden">
<div class="blob-1"></div>
<div class="blob-2"></div>
</div>
<div class="min-h-screen relative z-0 flex flex-col">
<div class="sticky top-0 z-50 backdrop-blur-md bg-white/70 dark:bg-slate-900/70 border-b border-slate-200/50 dark:border-slate-800/50 transition-all duration-300">
@include('layouts.navigation')
</div>
<!-- Page Heading -->
@isset($header)
<header class="bg-white dark:bg-gray-800 shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<header class="bg-white/80 dark:bg-slate-900/80 backdrop-blur-sm border-b border-slate-200/50 dark:border-slate-800/50 shadow-sm relative z-10">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8 animate-fade-in-up">
{{ $header }}
</div>
</header>
@endisset
<!-- Page Content -->
<main>
<main class="flex-grow">
{{ $slot }}
</main>
</div>
+76 -9
View File
@@ -1,28 +1,95 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="scroll-smooth">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<title>{{ config('app.name', 'WebGIS Pengentasan Kemiskinan') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700,800|outfit:500,600,700,800" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
body {
font-family: 'Inter', sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Outfit', sans-serif;
}
.bg-grid-pattern {
background-image: linear-gradient(to right, rgba(0,0,0,0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(0,0,0,0.05) 1px, transparent 1px);
background-size: 40px 40px;
}
.dark .bg-grid-pattern {
background-image: linear-gradient(to right, rgba(255,255,255,0.03) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255,255,255,0.03) 1px, transparent 1px);
}
.blob-1 {
position: absolute;
top: -10%;
left: -10%;
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(99,102,241,0.15) 0%, rgba(transparent) 70%);
border-radius: 50%;
filter: blur(40px);
z-index: -1;
}
.blob-2 {
position: absolute;
bottom: -10%;
right: -10%;
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(168,85,247,0.15) 0%, rgba(transparent) 70%);
border-radius: 50%;
filter: blur(40px);
z-index: -1;
}
.dark .blob-1 { background: radial-gradient(circle, rgba(99,102,241,0.2) 0%, rgba(transparent) 70%); }
.dark .blob-2 { background: radial-gradient(circle, rgba(168,85,247,0.2) 0%, rgba(transparent) 70%); }
.animate-fade-in-up {
animation: fadeInUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
opacity: 0;
transform: translateY(20px);
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</head>
<body class="font-sans text-gray-900 antialiased">
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
<div>
<a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
<body class="antialiased bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 relative overflow-x-hidden selection:bg-indigo-500 selection:text-white min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0">
<!-- Background Elements -->
<div class="fixed inset-0 bg-grid-pattern pointer-events-none z-[-2]"></div>
<div class="fixed inset-0 pointer-events-none z-[-1] overflow-hidden">
<div class="blob-1"></div>
<div class="blob-2"></div>
</div>
<div class="animate-fade-in-up w-full flex flex-col items-center">
<div class="mb-8">
<a href="/" class="flex flex-col items-center gap-3 group">
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white shadow-lg shadow-indigo-500/30 group-hover:scale-105 transition-transform duration-300">
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<span class="font-outfit font-bold text-2xl tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-indigo-600 to-purple-600 dark:from-indigo-400 dark:to-purple-400">
WebGIS Kesra
</span>
</a>
</div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg">
<div class="w-full sm:max-w-md px-8 py-10 bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl border border-slate-200 dark:border-slate-800 shadow-2xl overflow-hidden sm:rounded-2xl">
{{ $slot }}
</div>
</div>
+55 -16
View File
@@ -1,38 +1,61 @@
<nav x-data="{ open: false }" class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700">
<nav x-data="{ open: false }" class="bg-transparent border-none">
<!-- 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">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="{{ route('dashboard') }}">
<x-application-logo class="block h-9 w-auto fill-current text-gray-800 dark:text-gray-200" />
<a href="{{ route('dashboard') }}" class="group flex items-center gap-2">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white shadow-lg shadow-indigo-500/30 group-hover:scale-105 transition-transform duration-300">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<span class="font-outfit font-bold text-xl tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-indigo-600 to-purple-600 dark:from-indigo-400 dark:to-purple-400 hidden sm:block">
WebGIS Kesra
</span>
</a>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex font-medium font-outfit">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
@if(Auth::user()->role === 'admin')
<x-nav-link :href="route('ibadah.index')" :active="request()->routeIs('ibadah.index')">
{{ __('Daftar Tempat Ibadah') }}
</x-nav-link>
@endif
<x-nav-link :href="route('penerima.index')" :active="request()->routeIs('penerima.index')">
{{ __('Daftar Penerima Bantuan') }}
</x-nav-link>
@if(Auth::user()->role !== 'admin')
<x-nav-link :href="route('laporan.index')" :active="request()->routeIs('laporan.*')">
{{ __('Laporan Warga') }}
</x-nav-link>
<x-nav-link :href="route('scan.index')" :active="request()->routeIs('scan.index')">
{{ __('Scan Kupon') }}
</x-nav-link>
@endif
<x-nav-link :href="route('riwayat.index')" :active="request()->routeIs('riwayat.*')">
{{ __('Riwayat Penyaluran') }}
</x-nav-link>
</div>
</div>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ms-6">
<div class="hidden sm:flex sm:items-center sm:ms-6 font-outfit">
<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 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150">
<button class="inline-flex items-center px-4 py-2 border border-slate-200/50 dark:border-slate-700/50 text-sm leading-4 font-medium rounded-xl text-slate-600 dark:text-slate-300 bg-white/50 dark:bg-slate-800/50 hover:bg-slate-50 hover:text-slate-800 dark:hover:bg-slate-800 dark:hover:text-slate-200 focus:outline-none transition ease-in-out duration-150 backdrop-blur-md shadow-sm">
<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">
<div class="ms-2 bg-slate-200/50 dark:bg-slate-700/50 rounded-full p-1">
<svg class="fill-current h-3 w-3" 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>
</div>
@@ -60,7 +83,7 @@
<!-- 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 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-xl text-slate-400 dark:text-slate-500 hover:text-slate-500 dark:hover:text-slate-400 hover:bg-slate-100/50 dark:hover:bg-slate-800/50 focus:outline-none focus:bg-slate-100/50 dark:focus:bg-slate-800/50 focus:text-slate-500 dark:focus:text-slate-400 transition duration-150 ease-in-out backdrop-blur-sm border border-transparent hover:border-slate-200 dark:hover:border-slate-700">
<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" />
@@ -71,27 +94,43 @@
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl border-b border-slate-200/50 dark:border-slate-800/50 absolute w-full shadow-lg">
<div class="pt-2 pb-3 space-y-1 font-outfit">
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
<x-responsive-nav-link :href="route('penerima.index')" :active="request()->routeIs('penerima.index')">
{{ __('Daftar Penerima Bantuan') }}
</x-responsive-nav-link>
@if(Auth::user()->role !== 'admin')
<x-responsive-nav-link :href="route('laporan.index')" :active="request()->routeIs('laporan.*')">
{{ __('Laporan Warga') }}
</x-responsive-nav-link>
<x-responsive-nav-link :href="route('scan.index')" :active="request()->routeIs('scan.index')">
{{ __('Scan Kupon') }}
</x-responsive-nav-link>
@endif
@if(Auth::user()->role === 'admin')
<x-responsive-nav-link :href="route('ibadah.index')" :active="request()->routeIs('ibadah.index')">
{{ __('Daftar Tempat Ibadah') }}
</x-responsive-nav-link>
@endif
<x-responsive-nav-link :href="route('riwayat.index')" :active="request()->routeIs('riwayat.*')">
{{ __('Riwayat Penyaluran') }}
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
<div class="px-4">
<div class="font-medium text-base text-gray-800 dark:text-gray-200">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
<div class="pt-4 pb-1 border-t border-slate-200/50 dark:border-slate-700/50">
<div class="px-4 pb-2">
<div class="font-bold font-outfit text-base text-slate-800 dark:text-slate-200">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-slate-500 dark:text-slate-400">{{ Auth::user()->email }}</div>
</div>
<div class="mt-3 space-y-1">
<div class="mt-3 space-y-1 font-outfit">
<x-responsive-nav-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-responsive-nav-link>
+127
View File
@@ -0,0 +1,127 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Riwayat Penyaluran Bantuan') }}
</h2>
</x-slot>
<div class="py-8">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-8">
<!-- Description & Filter Card -->
<div class="bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl overflow-hidden shadow-xl sm:rounded-3xl border border-slate-200/50 dark:border-slate-800/50 p-6 md:p-8">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h3 class="text-2xl font-bold font-outfit text-slate-900 dark:text-white mb-1 tracking-tight">Daftar Pencairan Bantuan</h3>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">
Catatan waktu dan identitas penerima yang telah berhasil mencairkan bantuan melalui pemindaian QR Code.
</p>
</div>
<form method="GET" action="" class="flex flex-wrap items-center gap-3">
<div class="relative w-full sm:w-auto">
<input type="month" name="filter_periode" id="filter_periode"
value="{{ request('filter_periode', '') }}"
onchange="this.form.submit()"
class="bg-white dark:bg-slate-800/50 border border-slate-300 text-slate-900 text-sm rounded-xl focus:ring-indigo-500 focus:border-indigo-500 block w-full px-4 py-2.5 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white shadow-sm transition-colors cursor-pointer appearance-none">
</div>
@if(request('filter_periode'))
<a href="{{ route('riwayat.cetakPdf', ['periode' => request('filter_periode')]) }}" target="_blank" class="w-full sm:w-auto inline-flex items-center justify-center px-5 py-2.5 bg-gradient-to-r from-emerald-500 to-green-600 hover:from-emerald-600 hover:to-green-700 text-white rounded-xl font-medium shadow-lg shadow-emerald-500/30 hover:-translate-y-0.5 transition-all text-sm whitespace-nowrap">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"></path></svg>
Cetak PDF
</a>
@endif
</form>
</div>
</div>
<!-- Cards Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@forelse($riwayats as $r)
@php
$nama_kk = $r->penerimaBantuan->nama_kepala_keluarga ?? 'Data Terhapus';
$nik = $r->penerimaBantuan && $r->penerimaBantuan->nik_kepala_keluarga
? substr($r->penerimaBantuan->nik_kepala_keluarga, 0, 6) . '**********'
: '-';
$alamat = $r->penerimaBantuan->alamat ?? '-';
$penyalur = $r->penerimaBantuan && $r->penerimaBantuan->tempat_ibadah
? $r->penerimaBantuan->tempat_ibadah->nama_tempat : '-';
$isMenunggu = $r->status == 'Menunggu';
@endphp
<div class="bg-white/60 dark:bg-slate-800/60 backdrop-blur-md border {{ $isMenunggu ? 'border-amber-300/50 dark:border-amber-700/50' : 'border-slate-200/80 dark:border-slate-700/80' }} rounded-3xl shadow-lg shadow-slate-200/50 dark:shadow-slate-900/50 hover:shadow-xl hover:-translate-y-1 transition-all duration-300 flex flex-col overflow-hidden group">
<!-- Card Header -->
<div class="px-6 py-5 border-b {{ $isMenunggu ? 'border-amber-100 dark:border-amber-800/30 bg-amber-50/80 dark:bg-amber-900/20' : 'border-indigo-100 dark:border-indigo-800/30 bg-indigo-50/80 dark:bg-indigo-900/30' }} flex justify-between items-center transition-colors">
<div class="flex items-center text-sm font-semibold text-slate-900 dark:text-white">
<svg class="w-4 h-4 mr-2 {{ $isMenunggu ? 'text-amber-500' : 'text-indigo-500' }}" fill="none" stroke="currentColor" 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"></path></svg>
{{ $r->tanggal_diterima->format('d M Y, H:i') }}
</div>
<span class="px-3 py-1.5 text-xs font-bold rounded-lg {{ $isMenunggu ? 'bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300 border border-amber-200 dark:border-amber-800' : 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-800' }}">
{{ $r->status }}
</span>
</div>
<!-- Card Body -->
<div class="p-6 flex-grow space-y-4 text-sm">
<div>
<p class="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wider font-semibold mb-1">Nama Kepala Keluarga</p>
<p class="text-base font-bold font-outfit text-slate-900 dark:text-white">{{ $nama_kk }}</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wider font-semibold mb-1">NIK</p>
<p class="text-slate-800 dark:text-slate-200 font-mono font-medium">{{ $nik }}</p>
</div>
<div>
<p class="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wider font-semibold mb-1">Penyalur</p>
<p class="text-slate-800 dark:text-slate-200 truncate font-medium" title="{{ $penyalur }}">{{ $penyalur }}</p>
</div>
</div>
<div class="border-t border-slate-100 dark:border-slate-700/50 pt-3">
<p class="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wider font-semibold mb-1">Alamat</p>
<p class="text-slate-800 dark:text-slate-300 line-clamp-2 leading-relaxed" title="{{ $alamat }}">{{ $alamat }}</p>
</div>
@if(!empty($r->foto_penyerahan))
<div class="pt-3 border-t border-slate-100 dark:border-slate-700/50">
<p class="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wider font-semibold mb-2">Bukti Foto Penyerahan</p>
<a href="{{ asset('storage/uploads/' . $r->foto_penyerahan) }}" target="_blank" class="block rounded-xl overflow-hidden border border-slate-200 dark:border-slate-700 bg-slate-100 dark:bg-slate-900 hover:opacity-90 transition-all shadow-inner aspect-video relative group">
<img src="{{ asset('storage/uploads/' . $r->foto_penyerahan) }}" class="w-full h-full object-cover" alt="Bukti Penyerahan">
<div class="absolute inset-0 bg-slate-900/40 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-all backdrop-blur-[2px]">
<div class="bg-white/20 p-2 rounded-full backdrop-blur-md">
<svg class="w-6 h-6 text-white" 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 0zM10 7v3m0 0v3m0-3h3m-3 0H7"></path></svg>
</div>
</div>
</a>
</div>
@endif
</div>
<!-- Card Footer -->
@if($isMenunggu)
<div class="px-6 py-4 bg-slate-50/50 dark:bg-slate-800/30 border-t border-slate-200/50 dark:border-slate-700/50 flex justify-end">
<a href="{{ route('riwayat.konfirmasi', $r->id) }}"
onclick="return confirm('Setujui bukti ini dan selesaikan penyaluran?')"
class="w-full flex justify-center items-center px-5 py-2.5 text-sm font-semibold text-white bg-gradient-to-r from-indigo-500 to-purple-600 rounded-xl hover:from-indigo-600 hover:to-purple-700 hover:-translate-y-0.5 transition-all shadow-lg shadow-indigo-500/30">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
Setujui Penyaluran
</a>
</div>
@endif
</div>
@empty
<div class="col-span-full flex flex-col items-center justify-center py-16 px-4 bg-white/40 dark:bg-slate-800/40 backdrop-blur-sm rounded-3xl border border-slate-200/50 dark:border-slate-700/50 shadow-sm text-center">
<div class="bg-slate-100 dark:bg-slate-700 rounded-full w-20 h-20 flex items-center justify-center mb-4 shadow-inner">
<svg class="w-10 h-10 text-slate-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"></path></svg>
</div>
<h3 class="text-lg font-bold text-slate-600 dark:text-slate-400 mb-1">Belum ada riwayat pencairan</h3>
</div>
@endforelse
</div>
</div>
</div>
</x-app-layout>
+111
View File
@@ -0,0 +1,111 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Scan Kupon Bantuan') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100 flex flex-col items-center">
<div class="text-center mb-6">
<h3 class="text-2xl font-bold text-gray-800 dark:text-white mb-2">Scanner Kupon QR Code</h3>
<p class="text-gray-500 dark:text-gray-400">Arahkan kamera ke QR Code kupon milik penerima bantuan untuk memverifikasi dan memproses pencairan.</p>
</div>
<div id="reader-container" class="w-full max-w-lg mx-auto bg-gray-50 dark:bg-gray-900 rounded-2xl overflow-hidden border-2 border-dashed border-gray-300 dark:border-gray-700 p-2">
<div id="reader" style="width: 100%;"></div>
</div>
<div id="scan-result" class="mt-6 w-full max-w-lg hidden">
<div class="bg-green-50 border border-green-200 rounded-xl p-4 flex items-center">
<div class="bg-green-100 text-green-600 rounded-full p-2 mr-4">
<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="M5 13l4 4L19 7"></path></svg>
</div>
<div>
<h4 class="font-bold text-green-800">QR Code Ditemukan!</h4>
<p class="text-sm text-green-700 mt-1">Mengarahkan ke halaman konfirmasi...</p>
</div>
</div>
</div>
<div id="scan-error" class="mt-6 w-full max-w-lg hidden">
<div class="bg-red-50 border border-red-200 rounded-xl p-4 flex items-center">
<div class="bg-red-100 text-red-600 rounded-full p-2 mr-4">
<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="M6 18L18 6M6 6l12 12"></path></svg>
</div>
<div>
<h4 class="font-bold text-red-800">QR Code Tidak Valid</h4>
<p class="text-sm text-red-700 mt-1" id="error-message">Ini bukan kupon bantuan yang valid.</p>
</div>
</div>
<button onclick="resumeScanning()" class="mt-3 w-full py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded-lg font-medium transition-colors">
Coba Scan Lagi
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Include html5-qrcode library -->
<script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
let html5QrcodeScanner = null;
function onScanSuccess(decodedText, decodedResult) {
// Hentikan scanner sementara
if (html5QrcodeScanner) {
html5QrcodeScanner.pause();
}
try {
// Validasi apakah ini URL dari sistem kita (mengandung /konfirmasi)
const url = new URL(decodedText);
if (url.pathname.includes('/konfirmasi')) {
// Tampilkan sukses
document.getElementById('scan-result').classList.remove('hidden');
document.getElementById('scan-error').classList.add('hidden');
// Redirect ke URL konfirmasi setelah 1 detik
setTimeout(() => {
window.location.href = decodedText;
}, 1000);
} else {
throw new Error("Bukan QR Code Kupon dari sistem ini.");
}
} catch (e) {
// Tampilkan error
document.getElementById('scan-result').classList.add('hidden');
document.getElementById('scan-error').classList.remove('hidden');
}
}
function onScanFailure(error) {
// handle scan failure, usually better to ignore and keep scanning
// console.warn(`Code scan error = ${error}`);
}
html5QrcodeScanner = new Html5QrcodeScanner(
"reader",
{ fps: 10, qrbox: {width: 250, height: 250} },
/* verbose= */ false
);
html5QrcodeScanner.render(onScanSuccess, onScanFailure);
window.resumeScanning = function() {
document.getElementById('scan-error').classList.add('hidden');
if (html5QrcodeScanner) {
html5QrcodeScanner.resume();
}
};
});
</script>
</x-app-layout>
File diff suppressed because one or more lines are too long
+31
View File
@@ -4,11 +4,24 @@ use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\MapController;
use App\Http\Controllers\PersetujuanController;
use App\Http\Controllers\RiwayatController;
use App\Http\Controllers\KuponController;
use App\Http\Controllers\KonfirmasiController;
Route::get('/', function () {
return view('welcome');
});
// Public routes (tanpa auth) - Konfirmasi penyerahan via QR
Route::get('/konfirmasi', [KonfirmasiController::class, 'show'])->name('konfirmasi.show');
Route::post('/konfirmasi', [KonfirmasiController::class, 'process'])->name('konfirmasi.process');
// Public routes (tanpa auth) - Pelaporan Warga
Route::get('/lapor/{kode_lapor}', [\App\Http\Controllers\LaporanController::class, 'create'])->name('lapor.create');
Route::post('/lapor/{kode_lapor}', [\App\Http\Controllers\LaporanController::class, 'store'])->name('lapor.store');
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dashboard', [MapController::class, 'index'])->name('dashboard');
Route::get('/daftar-penerima-bantuan', [MapController::class, 'daftarPenerimaBantuan'])->name('penerima.index');
@@ -17,6 +30,24 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::post('/map/update-radius', [MapController::class, 'updateRadius'])->name('map.updateRadius');
Route::post('/map/edit', [MapController::class, 'edit'])->name('map.edit');
Route::get('/map/delete', [MapController::class, 'delete'])->name('map.delete');
Route::post('/persetujuan/{id}', [PersetujuanController::class, 'updateStatus'])->name('persetujuan.update');
// Riwayat
Route::get('/riwayat', [RiwayatController::class, 'index'])->name('riwayat.index');
Route::get('/riwayat/konfirmasi/{id_log}', [RiwayatController::class, 'konfirmasiPenyaluran'])->name('riwayat.konfirmasi');
Route::get('/riwayat/cetak-pdf', [RiwayatController::class, 'cetakPdf'])->name('riwayat.cetakPdf');
// Kupon
Route::get('/kupon/{id}', [KuponController::class, 'show'])->name('kupon.show');
Route::post('/kupon/cetak', [KuponController::class, 'cetak'])->name('kupon.cetak');
// Scan Kupon
Route::get('/scan', [KonfirmasiController::class, 'scan'])->name('scan.index');
// Laporan Warga
Route::get('/laporan-warga', [\App\Http\Controllers\LaporanController::class, 'index'])->name('laporan.index');
});
Route::middleware('auth')->group(function () {
+13
View File
@@ -0,0 +1,13 @@
<?php
require 'vendor/autoload.php';
$app = require_once 'bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
foreach (\App\Models\TempatIbadah::all() as $t) {
if (!$t->kode_lapor) {
$t->update(['kode_lapor' => \Illuminate\Support\Str::random(10)]);
echo "Updated {$t->id} with {$t->kode_lapor}\n";
}
}