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
+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.
*