Harden admin access and clean unused web controllers
This commit is contained in:
parent
864be692e8
commit
82227f5851
|
|
@ -39,7 +39,7 @@ public function bobot()
|
|||
$kriteriaKontrakan = Kriteria::where('tipe_bisnis', 'kontrakan')->get();
|
||||
$kriteriaLaundry = Kriteria::where('tipe_bisnis', 'laundry')->get();
|
||||
|
||||
return view('SAW.bobot', compact('kriteriaKontrakan', 'kriteriaLaundry'));
|
||||
return view('saw.bobot', compact('kriteriaKontrakan', 'kriteriaLaundry'));
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error di SAW Bobot: ' . $e->getMessage());
|
||||
|
|
|
|||
|
|
@ -1,101 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class UserAuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Halaman Login User
|
||||
*/
|
||||
public function loginPage()
|
||||
{
|
||||
// Jika sudah login sebagai user, redirect ke halaman utama
|
||||
if (Auth::check() && Auth::user()->role === 'user') {
|
||||
return redirect()->route('welcome');
|
||||
}
|
||||
|
||||
return view('auth.user-login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Proses Login User
|
||||
*/
|
||||
public function login(Request $request)
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required'
|
||||
]);
|
||||
|
||||
if (Auth::attempt($credentials)) {
|
||||
$user = Auth::user();
|
||||
|
||||
// Cek apakah yang login adalah user biasa
|
||||
if ($user->role !== 'user') {
|
||||
Auth::logout();
|
||||
return back()->with('error', 'Akun ini bukan akun user. Silakan login di halaman admin.');
|
||||
}
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
// Redirect ke halaman yang diminta atau ke welcome
|
||||
$redirect = $request->input('redirect', route('welcome'));
|
||||
return redirect($redirect)->with('success', 'Selamat datang, ' . $user->name . '!');
|
||||
}
|
||||
|
||||
return back()->with('error', 'Email atau password salah!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Halaman Register User
|
||||
*/
|
||||
public function registerPage()
|
||||
{
|
||||
return view('auth.user-register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Proses Register User
|
||||
*/
|
||||
public function register(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'password' => 'required|min:6|confirmed'
|
||||
]);
|
||||
|
||||
// Simpan ke tabel users dengan role USER
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'phone' => $request->phone,
|
||||
'password' => Hash::make($request->password),
|
||||
'role' => 'user' // PAKSA SELALU USER
|
||||
]);
|
||||
|
||||
// Auto login setelah register
|
||||
Auth::login($user);
|
||||
|
||||
return redirect()->route('welcome')->with('success', 'Akun berhasil dibuat! Selamat datang, ' . $user->name . '!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout User
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
{
|
||||
Auth::logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('welcome')->with('success', 'Anda telah logout.');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,325 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Booking;
|
||||
use App\Models\Kontrakan;
|
||||
use App\Models\Favorite;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Exception;
|
||||
|
||||
class UserBookingController extends Controller
|
||||
{
|
||||
/**
|
||||
* Tampilkan form booking untuk user
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$kontrakan = Kontrakan::findOrFail($request->kontrakan_id);
|
||||
|
||||
// Cek apakah kontrakan available
|
||||
if ($kontrakan->status !== 'available') {
|
||||
return redirect()->back()->with('error', 'Maaf, kontrakan ini sedang tidak tersedia.');
|
||||
}
|
||||
|
||||
return view('user.booking.create', compact('kontrakan'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Simpan booking dari user
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
// Debug: Log all incoming data
|
||||
\Log::info('Booking store request', [
|
||||
'all_data' => $request->all(),
|
||||
'has_file' => $request->hasFile('payment_proof'),
|
||||
'file_info' => $request->file('payment_proof') ? [
|
||||
'name' => $request->file('payment_proof')->getClientOriginalName(),
|
||||
'size' => $request->file('payment_proof')->getSize(),
|
||||
'mime' => $request->file('payment_proof')->getMimeType(),
|
||||
] : 'no file',
|
||||
'files' => $_FILES,
|
||||
]);
|
||||
|
||||
$request->validate([
|
||||
'kontrakan_id' => 'required|exists:kontrakans,id',
|
||||
'tenant_name' => 'required|string|max:255',
|
||||
'tenant_phone' => 'required|string|max:20',
|
||||
'start_date' => 'required|date|after_or_equal:today',
|
||||
'duration' => 'required|integer|min:1|max:24',
|
||||
'payment_proof' => 'required|image|mimes:jpeg,png,jpg|max:5120',
|
||||
'notes' => 'nullable|string|max:1000',
|
||||
], [
|
||||
'tenant_name.required' => 'Nama lengkap wajib diisi.',
|
||||
'tenant_phone.required' => 'Nomor HP/WhatsApp wajib diisi.',
|
||||
'start_date.required' => 'Tanggal mulai sewa wajib diisi.',
|
||||
'start_date.after_or_equal' => 'Tanggal mulai tidak boleh kurang dari hari ini.',
|
||||
'duration.required' => 'Durasi sewa wajib diisi.',
|
||||
'duration.min' => 'Durasi sewa minimal 1 bulan.',
|
||||
'payment_proof.required' => 'Bukti pembayaran wajib diupload.',
|
||||
'payment_proof.image' => 'File harus berupa gambar.',
|
||||
'payment_proof.max' => 'Ukuran file maksimal 5MB.',
|
||||
]);
|
||||
|
||||
try {
|
||||
$kontrakan = Kontrakan::findOrFail($request->kontrakan_id);
|
||||
|
||||
// Cast duration ke integer
|
||||
$duration = (int) $request->duration;
|
||||
|
||||
// Hitung tanggal selesai berdasarkan durasi (bulan)
|
||||
$startDate = \Carbon\Carbon::parse($request->start_date);
|
||||
$endDate = $startDate->copy()->addMonths($duration);
|
||||
|
||||
// Hitung total biaya (harga per tahun / 12 * durasi bulan)
|
||||
$amount = ($kontrakan->harga / 12) * $duration;
|
||||
|
||||
// Simpan user_id dan kontrakan_id untuk digunakan di closure
|
||||
$userId = Auth::id();
|
||||
$kontrakanId = $request->kontrakan_id;
|
||||
|
||||
$booking = DB::transaction(function () use ($request, $kontrakan, $startDate, $endDate, $amount, $userId, $kontrakanId) {
|
||||
// Lock kontrakan
|
||||
$kontrakan = Kontrakan::lockForUpdate()->findOrFail($kontrakanId);
|
||||
|
||||
// Cek konflik
|
||||
$hasConflict = Booking::hasConflict(
|
||||
$kontrakanId,
|
||||
$startDate->format('Y-m-d'),
|
||||
$endDate->format('Y-m-d')
|
||||
);
|
||||
|
||||
if ($hasConflict) {
|
||||
throw new Exception('Maaf, kontrakan sudah dipesan untuk periode tersebut. Silakan pilih tanggal lain.');
|
||||
}
|
||||
|
||||
// Upload bukti pembayaran
|
||||
$paymentProofPath = null;
|
||||
if ($request->hasFile('payment_proof')) {
|
||||
$file = $request->file('payment_proof');
|
||||
$filename = 'payment_' . time() . '_' . uniqid() . '.' . $file->getClientOriginalExtension();
|
||||
$paymentProofPath = $file->storeAs('payment_proofs', $filename, 'public');
|
||||
}
|
||||
|
||||
// Buat booking
|
||||
$booking = Booking::create([
|
||||
'kontrakan_id' => $kontrakanId,
|
||||
'user_id' => $userId,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'tenant_name' => $request->tenant_name,
|
||||
'tenant_phone' => $request->tenant_phone,
|
||||
'amount' => $amount,
|
||||
'payment_proof' => $paymentProofPath,
|
||||
'payment_status' => Booking::PAYMENT_UNPAID,
|
||||
'status' => Booking::STATUS_PENDING,
|
||||
'booking_source' => 'user',
|
||||
'notes' => $request->notes,
|
||||
]);
|
||||
|
||||
// Hapus dari favorit setelah booking berhasil
|
||||
Favorite::where('user_id', $userId)
|
||||
->where('type', 'kontrakan')
|
||||
->where('item_id', $kontrakanId)
|
||||
->delete();
|
||||
|
||||
return $booking;
|
||||
});
|
||||
|
||||
return redirect()->route('user.booking.success', $booking->id)
|
||||
->with('success', 'Booking berhasil dikirim! Mohon tunggu konfirmasi dari pemilik kontrakan.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
return redirect()->back()
|
||||
->withInput()
|
||||
->with('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Halaman sukses booking
|
||||
*/
|
||||
public function success($id)
|
||||
{
|
||||
$booking = Booking::with('kontrakan')->findOrFail($id);
|
||||
|
||||
// Pastikan booking milik user yang login atau baru dibuat
|
||||
if ($booking->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return view('user.booking.success', compact('booking'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Riwayat booking user
|
||||
*/
|
||||
public function history()
|
||||
{
|
||||
$bookings = Booking::with('kontrakan')
|
||||
->where('user_id', Auth::id())
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
return view('user.booking.history', compact('bookings'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Detail booking user
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
$booking = Booking::with('kontrakan')->findOrFail($id);
|
||||
|
||||
if ($booking->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return view('user.booking.show', compact('booking'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel booking oleh user (hanya jika masih pending)
|
||||
*/
|
||||
public function cancel(Request $request, $id)
|
||||
{
|
||||
$booking = Booking::findOrFail($id);
|
||||
|
||||
if ($booking->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if ($booking->status !== Booking::STATUS_PENDING) {
|
||||
return redirect()->back()->with('error', 'Booking tidak dapat dibatalkan.');
|
||||
}
|
||||
|
||||
$booking->update([
|
||||
'status' => Booking::STATUS_CANCELLED,
|
||||
'cancelled_at' => now(),
|
||||
'cancellation_reason' => 'Dibatalkan oleh penyewa',
|
||||
]);
|
||||
|
||||
return redirect()->route('user.booking.history')
|
||||
->with('success', 'Booking berhasil dibatalkan.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Form perpanjang kontrak
|
||||
*/
|
||||
public function extend($id)
|
||||
{
|
||||
$booking = Booking::with('kontrakan')->findOrFail($id);
|
||||
|
||||
// Pastikan booking milik user yang login
|
||||
if ($booking->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
// Hanya booking yang completed atau checked_in yang bisa diperpanjang
|
||||
if (!in_array($booking->status, [Booking::STATUS_COMPLETED, Booking::STATUS_CHECKED_IN, Booking::STATUS_CONFIRMED])) {
|
||||
return redirect()->back()->with('error', 'Booking ini tidak dapat diperpanjang.');
|
||||
}
|
||||
|
||||
// Cek apakah kontrakan masih ada
|
||||
if (!$booking->kontrakan) {
|
||||
return redirect()->back()->with('error', 'Kontrakan tidak ditemukan.');
|
||||
}
|
||||
|
||||
return view('user.booking.extend', compact('booking'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Simpan perpanjangan kontrak
|
||||
*/
|
||||
public function storeExtension(Request $request, $id)
|
||||
{
|
||||
$originalBooking = Booking::with('kontrakan')->findOrFail($id);
|
||||
|
||||
// Pastikan booking milik user yang login
|
||||
if ($originalBooking->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'duration' => 'required|integer|min:1|max:24',
|
||||
'payment_proof' => 'required|image|mimes:jpeg,png,jpg|max:5120',
|
||||
'notes' => 'nullable|string|max:1000',
|
||||
], [
|
||||
'duration.required' => 'Durasi perpanjangan wajib diisi.',
|
||||
'duration.min' => 'Durasi minimal 1 bulan.',
|
||||
'payment_proof.required' => 'Bukti pembayaran wajib diupload.',
|
||||
'payment_proof.image' => 'File harus berupa gambar.',
|
||||
'payment_proof.max' => 'Ukuran file maksimal 5MB.',
|
||||
]);
|
||||
|
||||
try {
|
||||
$kontrakan = $originalBooking->kontrakan;
|
||||
$duration = (int) $request->duration;
|
||||
|
||||
// Tanggal mulai = tanggal selesai booking sebelumnya
|
||||
$startDate = $originalBooking->end_date->copy();
|
||||
|
||||
// Jika tanggal selesai sudah lewat, mulai dari hari ini
|
||||
if ($startDate->isPast()) {
|
||||
$startDate = \Carbon\Carbon::today();
|
||||
}
|
||||
|
||||
$endDate = $startDate->copy()->addMonths($duration);
|
||||
|
||||
// Hitung total biaya
|
||||
$amount = ($kontrakan->harga / 12) * $duration;
|
||||
|
||||
$booking = DB::transaction(function () use ($request, $originalBooking, $kontrakan, $startDate, $endDate, $amount) {
|
||||
// Cek konflik jadwal
|
||||
$hasConflict = Booking::hasConflict(
|
||||
$kontrakan->id,
|
||||
$startDate->format('Y-m-d'),
|
||||
$endDate->format('Y-m-d'),
|
||||
$originalBooking->id // exclude original booking
|
||||
);
|
||||
|
||||
if ($hasConflict) {
|
||||
throw new Exception('Maaf, kontrakan sudah dipesan untuk periode tersebut.');
|
||||
}
|
||||
|
||||
// Upload bukti pembayaran
|
||||
$paymentProofPath = null;
|
||||
if ($request->hasFile('payment_proof')) {
|
||||
$file = $request->file('payment_proof');
|
||||
$filename = 'payment_' . time() . '_' . uniqid() . '.' . $file->getClientOriginalExtension();
|
||||
$paymentProofPath = $file->storeAs('payment_proofs', $filename, 'public');
|
||||
}
|
||||
|
||||
// Buat booking baru sebagai perpanjangan
|
||||
$booking = Booking::create([
|
||||
'kontrakan_id' => $kontrakan->id,
|
||||
'user_id' => Auth::id(),
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'tenant_name' => $originalBooking->tenant_name,
|
||||
'tenant_phone' => $originalBooking->tenant_phone,
|
||||
'amount' => $amount,
|
||||
'payment_proof' => $paymentProofPath,
|
||||
'payment_status' => Booking::PAYMENT_UNPAID,
|
||||
'status' => Booking::STATUS_PENDING,
|
||||
'booking_source' => 'user',
|
||||
'notes' => $request->notes ? "Perpanjangan dari Booking #{$originalBooking->id}. " . $request->notes : "Perpanjangan dari Booking #{$originalBooking->id}",
|
||||
]);
|
||||
|
||||
return $booking;
|
||||
});
|
||||
|
||||
return redirect()->route('user.booking.success', $booking->id)
|
||||
->with('success', 'Perpanjangan kontrak berhasil diajukan! Menunggu konfirmasi admin.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
return redirect()->back()
|
||||
->withInput()
|
||||
->withErrors(['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,699 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Kontrakan;
|
||||
use App\Models\Laundry;
|
||||
use App\Models\Kriteria;
|
||||
use App\Models\Booking;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Exception;
|
||||
|
||||
class UserSAWController extends Controller
|
||||
{
|
||||
// Koordinat Kampus Polije (FIXED)
|
||||
const KAMPUS_LAT = -8.15981;
|
||||
const KAMPUS_LNG = 113.72312;
|
||||
|
||||
/**
|
||||
* Tampilkan halaman input preferensi user (Hybrid System)
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
// Ambil kriteria untuk kontrakan
|
||||
$kriteria = Kriteria::where('tipe_bisnis', 'kontrakan')->get();
|
||||
|
||||
if ($kriteria->isEmpty()) {
|
||||
return view('user.preferensi')->with('error', 'Data kriteria belum tersedia. Hubungi administrator.');
|
||||
}
|
||||
|
||||
// Ambil ID kontrakan yang sedang di-booking aktif
|
||||
$bookedKontrakanIds = Booking::whereIn('status', [
|
||||
Booking::STATUS_PENDING,
|
||||
Booking::STATUS_CONFIRMED,
|
||||
Booking::STATUS_CHECKED_IN
|
||||
])->pluck('kontrakan_id')->toArray();
|
||||
|
||||
// Ambil kontrakan yang tersedia (tidak sedang di-booking)
|
||||
$kontrakans = Kontrakan::whereNotIn('id', $bookedKontrakanIds)->get();
|
||||
|
||||
// Hitung total dan yang tersedia untuk info
|
||||
$totalKontrakan = Kontrakan::count();
|
||||
$availableCount = $kontrakans->count();
|
||||
$bookedCount = count($bookedKontrakanIds);
|
||||
|
||||
return view('user.preferensi', compact('kriteria', 'kontrakans', 'totalKontrakan', 'availableCount', 'bookedCount'));
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error di User SAW Index: ' . $e->getMessage());
|
||||
return redirect()->back()->with('error', 'Terjadi kesalahan. Silakan coba lagi.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proses perhitungan SAW dengan bobot hybrid (10-70%)
|
||||
*/
|
||||
public function calculate(Request $request)
|
||||
{
|
||||
try {
|
||||
// Validasi input bobot (sekarang dalam %)
|
||||
$request->validate([
|
||||
'bobot_harga' => 'required|integer|min:10|max:70',
|
||||
'bobot_jarak' => 'required|integer|min:10|max:70',
|
||||
'bobot_jumlah_kamar' => 'required|integer|min:10|max:70',
|
||||
'bobot_fasilitas' => 'required|integer|min:10|max:70',
|
||||
'mode' => 'required|in:preset,manual',
|
||||
], [
|
||||
'bobot_harga.min' => 'Bobot harga minimal 10%!',
|
||||
'bobot_harga.max' => 'Bobot harga maksimal 70%!',
|
||||
'bobot_jarak.min' => 'Bobot jarak minimal 10%!',
|
||||
'bobot_jarak.max' => 'Bobot jarak maksimal 70%!',
|
||||
'bobot_jumlah_kamar.min' => 'Bobot jumlah kamar minimal 10%!',
|
||||
'bobot_jumlah_kamar.max' => 'Bobot jumlah kamar maksimal 70%!',
|
||||
'bobot_fasilitas.min' => 'Bobot fasilitas minimal 10%!',
|
||||
'bobot_fasilitas.max' => 'Bobot fasilitas maksimal 70%!',
|
||||
]);
|
||||
|
||||
// Validasi total bobot harus 100%
|
||||
$totalBobot = $request->bobot_harga + $request->bobot_jarak +
|
||||
$request->bobot_jumlah_kamar + $request->bobot_fasilitas;
|
||||
|
||||
if ($totalBobot != 100) {
|
||||
return back()->withErrors([
|
||||
'bobot' => "Total bobot harus tepat 100%. Sekarang: {$totalBobot}%"
|
||||
])->withInput();
|
||||
}
|
||||
|
||||
// Gunakan koordinat kampus sebagai referensi (FIXED)
|
||||
$refLat = self::KAMPUS_LAT;
|
||||
$refLng = self::KAMPUS_LNG;
|
||||
$refName = 'Kampus Polije';
|
||||
|
||||
// Convert bobot ke decimal (0-1)
|
||||
$bobot = [
|
||||
'harga' => $request->bobot_harga / 100,
|
||||
'jarak' => $request->bobot_jarak / 100,
|
||||
'jumlah_kamar' => $request->bobot_jumlah_kamar / 100,
|
||||
'fasilitas' => $request->bobot_fasilitas / 100,
|
||||
];
|
||||
|
||||
Log::info('User SAW Hybrid Input:', [
|
||||
'bobot' => $bobot,
|
||||
'reference_point' => $refName,
|
||||
'ref_lat' => $refLat,
|
||||
'ref_lng' => $refLng,
|
||||
]);
|
||||
|
||||
// Ambil kriteria kontrakan
|
||||
$kriteria = Kriteria::where('tipe_bisnis', 'kontrakan')->get();
|
||||
|
||||
if ($kriteria->isEmpty()) {
|
||||
return redirect()->route('user.search')
|
||||
->with('error', 'Kriteria belum tersedia!');
|
||||
}
|
||||
|
||||
// Ambil ID kontrakan yang sedang di-booking aktif (pending, confirmed, checked_in)
|
||||
$bookedKontrakanIds = Booking::whereIn('status', [
|
||||
Booking::STATUS_PENDING,
|
||||
Booking::STATUS_CONFIRMED,
|
||||
Booking::STATUS_CHECKED_IN
|
||||
])->pluck('kontrakan_id')->toArray();
|
||||
|
||||
// Ambil semua kontrakan yang TERSEDIA (tidak sedang di-booking)
|
||||
$kontrakan = Kontrakan::whereNotIn('id', $bookedKontrakanIds)->get();
|
||||
|
||||
if ($kontrakan->isEmpty()) {
|
||||
return redirect()->route('user.search')
|
||||
->with('error', 'Semua kontrakan sedang tidak tersedia (sudah di-booking). Silakan coba lagi nanti.');
|
||||
}
|
||||
|
||||
// Proses data kontrakan dengan lokasi referensi
|
||||
$kontrakanProcessed = $this->processKontrakan($kontrakan, $refLat, $refLng);
|
||||
|
||||
// Hitung max/min
|
||||
$maxMin = $this->calculateMaxMin($kontrakanProcessed);
|
||||
|
||||
// Hitung SAW dengan bobot user
|
||||
$hasil = $this->calculateSAW($kontrakanProcessed, $kriteria, $bobot, $maxMin);
|
||||
|
||||
if (empty($hasil)) {
|
||||
return redirect()->route('user.search')
|
||||
->with('error', 'Gagal menghitung hasil. Silakan coba lagi.');
|
||||
}
|
||||
|
||||
// Info bobot untuk ditampilkan (TRANSPARANSI!)
|
||||
$bobotInfo = [
|
||||
'mode' => $request->mode,
|
||||
'preset_type' => $request->preset_type ?? null,
|
||||
'values' => [
|
||||
'harga' => $request->bobot_harga,
|
||||
'jarak' => $request->bobot_jarak,
|
||||
'kamar' => $request->bobot_jumlah_kamar,
|
||||
'fasilitas' => $request->bobot_fasilitas,
|
||||
],
|
||||
'reference_point' => $refName,
|
||||
'user_location' => ($request->user_lat && $request->user_lng) ? [
|
||||
'lat' => $request->user_lat,
|
||||
'lng' => $request->user_lng
|
||||
] : null
|
||||
];
|
||||
|
||||
return view('user.hasil', compact('hasil', 'kriteria', 'bobotInfo'));
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
return redirect()->route('user.search')
|
||||
->withErrors($e->validator)
|
||||
->withInput();
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error di User SAW Calculate: ' . $e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return redirect()->route('user.search')
|
||||
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proses data kontrakan dengan hitung fasilitas & jarak
|
||||
*/
|
||||
private function processKontrakan($kontrakan, $refLat, $refLng)
|
||||
{
|
||||
return $kontrakan->map(function($item) use ($refLat, $refLng) {
|
||||
try {
|
||||
// Hitung jumlah fasilitas
|
||||
$item->jumlah_fasilitas = $item->fasilitas ? count(explode(',', $item->fasilitas)) : 0;
|
||||
|
||||
// Set harga
|
||||
$item->harga_value = $item->harga ?? 0;
|
||||
|
||||
// Hitung jarak dari lokasi referensi
|
||||
if ($item->latitude && $item->longitude) {
|
||||
try {
|
||||
// calculateDistance mengembalikan jarak dalam km, konversi ke meter
|
||||
$item->jarak_value = $item->calculateDistance($refLat, $refLng) * 1000;
|
||||
} catch (Exception $e) {
|
||||
Log::warning("Gagal hitung jarak untuk {$item->nama}: " . $e->getMessage());
|
||||
$item->jarak_value = $item->jarak ?? 0;
|
||||
}
|
||||
} else {
|
||||
$item->jarak_value = $item->jarak ?? 0;
|
||||
}
|
||||
|
||||
// Set jumlah_kamar
|
||||
$item->jumlah_kamar_value = $item->jumlah_kamar ?? 0;
|
||||
|
||||
return $item;
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error processing kontrakan {$item->id}: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
})->filter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitung max/min untuk normalisasi
|
||||
*/
|
||||
private function calculateMaxMin($kontrakan)
|
||||
{
|
||||
return [
|
||||
'harga' => [
|
||||
'max' => $kontrakan->max('harga_value') ?: 1,
|
||||
'min' => $kontrakan->min('harga_value') ?: 1,
|
||||
],
|
||||
'jarak' => [
|
||||
'max' => $kontrakan->max('jarak_value') ?: 1,
|
||||
'min' => $kontrakan->min('jarak_value') ?: 1,
|
||||
],
|
||||
'jumlah_kamar' => [
|
||||
'max' => $kontrakan->max('jumlah_kamar_value') ?: 1,
|
||||
'min' => $kontrakan->min('jumlah_kamar_value') ?: 1,
|
||||
],
|
||||
'fasilitas' => [
|
||||
'max' => $kontrakan->max('jumlah_fasilitas') ?: 1,
|
||||
'min' => $kontrakan->min('jumlah_fasilitas') ?: 1,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitung SAW dengan bobot dari user
|
||||
*/
|
||||
private function calculateSAW($kontrakan, $kriteria, $bobot, $maxMin)
|
||||
{
|
||||
$hasil = [];
|
||||
|
||||
foreach ($kontrakan as $item) {
|
||||
try {
|
||||
$normalisasi = [];
|
||||
$nilaiTotal = 0;
|
||||
|
||||
// Hitung untuk setiap kriteria
|
||||
$kriteriaMap = [
|
||||
'harga' => ['value' => $item->harga_value, 'maxmin' => $maxMin['harga']],
|
||||
'jarak' => ['value' => $item->jarak_value, 'maxmin' => $maxMin['jarak']],
|
||||
'jumlah_kamar' => ['value' => $item->jumlah_kamar_value, 'maxmin' => $maxMin['jumlah_kamar']],
|
||||
'fasilitas' => ['value' => $item->jumlah_fasilitas, 'maxmin' => $maxMin['fasilitas']],
|
||||
];
|
||||
|
||||
foreach ($kriteria as $krit) {
|
||||
$kriteriaKey = $this->getKriteriaKey($krit->nama_kriteria);
|
||||
|
||||
if (!isset($kriteriaMap[$kriteriaKey]) || !isset($bobot[$kriteriaKey])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$nilaiAsli = $kriteriaMap[$kriteriaKey]['value'];
|
||||
$maxMinKrit = $kriteriaMap[$kriteriaKey]['maxmin'];
|
||||
$bobotKrit = $bobot[$kriteriaKey];
|
||||
$tipeKrit = strtolower($krit->tipe);
|
||||
|
||||
$nilaiNormalisasi = $this->normalize($nilaiAsli, $maxMinKrit, $tipeKrit);
|
||||
|
||||
// Debug untuk jumlah kamar
|
||||
if ($kriteriaKey === 'jumlah_kamar') {
|
||||
Log::info("Debug Kamar - {$item->nama}: asli={$nilaiAsli}, max={$maxMinKrit['max']}, tipe={$tipeKrit}, normalisasi={$nilaiNormalisasi}");
|
||||
}
|
||||
|
||||
$normalisasi[$krit->nama_kriteria] = [
|
||||
'asli' => $nilaiAsli,
|
||||
'normalisasi' => round($nilaiNormalisasi, 4),
|
||||
'bobot' => $bobotKrit,
|
||||
'tipe' => $krit->tipe,
|
||||
];
|
||||
|
||||
$nilaiTotal += ($nilaiNormalisasi * $bobotKrit);
|
||||
}
|
||||
|
||||
$hasil[] = [
|
||||
'id' => $item->id,
|
||||
'nama' => $item->nama,
|
||||
'alamat' => $item->alamat,
|
||||
'harga' => $item->harga,
|
||||
'jarak' => $item->jarak_value,
|
||||
'jumlah_kamar' => $item->jumlah_kamar,
|
||||
'fasilitas' => $item->fasilitas,
|
||||
'jumlah_fasilitas' => $item->jumlah_fasilitas,
|
||||
'foto' => $item->foto,
|
||||
'latitude' => $item->latitude,
|
||||
'longitude' => $item->longitude,
|
||||
'no_whatsapp' => $item->no_whatsapp,
|
||||
'status' => $item->status ?? 'available', // Status ketersediaan
|
||||
'normalisasi' => $normalisasi,
|
||||
'nilai' => round($nilaiTotal, 4),
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error calculating SAW for kontrakan {$item->id}: " . $e->getMessage());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort berdasarkan nilai tertinggi
|
||||
usort($hasil, fn($a, $b) => $b['nilai'] <=> $a['nilai']);
|
||||
|
||||
// Tambahkan ranking
|
||||
foreach ($hasil as $key => $value) {
|
||||
$hasil[$key]['ranking'] = $key + 1;
|
||||
}
|
||||
|
||||
return $hasil;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalisasi nilai
|
||||
*/
|
||||
private function normalize($nilai, $maxMin, $tipe)
|
||||
{
|
||||
try {
|
||||
if ($nilai == 0 && $tipe == 'cost') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($tipe == 'cost') {
|
||||
return $nilai > 0 ? $maxMin['min'] / $nilai : 0;
|
||||
} else {
|
||||
return $maxMin['max'] > 0 ? $nilai / $maxMin['max'] : 0;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error normalize: ' . $e->getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bobot info untuk transparansi
|
||||
*/
|
||||
private function getBobotInfo($kriteria, $request)
|
||||
{
|
||||
$bobotInfo = [];
|
||||
foreach ($kriteria as $k) {
|
||||
$bobotInfo[] = [
|
||||
'nama' => $k->nama_kriteria,
|
||||
'bobot' => $request->input('bobot_' . $k->kode_kriteria, 50),
|
||||
];
|
||||
}
|
||||
return $bobotInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Convert nama kriteria ke key
|
||||
*/
|
||||
private function getKriteriaKey($namaKriteria)
|
||||
{
|
||||
$namaLower = strtolower($namaKriteria);
|
||||
|
||||
if (strpos($namaLower, 'harga') !== false) return 'harga';
|
||||
if (strpos($namaLower, 'jarak') !== false) return 'jarak';
|
||||
if (strpos($namaLower, 'jumlah_kamar') !== false ||
|
||||
strpos($namaLower, 'jumlah kamar') !== false ||
|
||||
strpos($namaLower, 'luas') !== false) return 'jumlah_kamar';
|
||||
if (strpos($namaLower, 'fasilitas') !== false) return 'fasilitas';
|
||||
|
||||
return explode(' ', $namaLower)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Halaman Preferensi SAW untuk Kontrakan
|
||||
*/
|
||||
public function preferensi()
|
||||
{
|
||||
try {
|
||||
$kriteria = Kriteria::where('tipe_bisnis', 'kontrakan')->get();
|
||||
|
||||
if ($kriteria->isEmpty()) {
|
||||
return redirect()->route('welcome')
|
||||
->with('error', 'Data kriteria belum tersedia. Hubungi administrator.');
|
||||
}
|
||||
|
||||
// Ambil ID kontrakan yang sedang di-booking aktif
|
||||
$bookedKontrakanIds = Booking::whereIn('status', [
|
||||
Booking::STATUS_PENDING,
|
||||
Booking::STATUS_CONFIRMED,
|
||||
Booking::STATUS_CHECKED_IN
|
||||
])->pluck('kontrakan_id')->toArray();
|
||||
|
||||
// Ambil kontrakan yang tersedia (tidak sedang di-booking)
|
||||
$kontrakans = Kontrakan::whereNotIn('id', $bookedKontrakanIds)->get();
|
||||
|
||||
// Hitung total dan yang tersedia untuk info
|
||||
$totalKontrakan = Kontrakan::count();
|
||||
$availableCount = $kontrakans->count();
|
||||
$bookedCount = count($bookedKontrakanIds);
|
||||
|
||||
return view('user.preferensi', compact('kriteria', 'kontrakans', 'totalKontrakan', 'availableCount', 'bookedCount'));
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error di User Preferensi: ' . $e->getMessage());
|
||||
return redirect()->route('welcome')->with('error', 'Terjadi kesalahan.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Halaman Preferensi SAW untuk Laundry
|
||||
*/
|
||||
public function preferensiLaundry()
|
||||
{
|
||||
try {
|
||||
$kriteria = Kriteria::where('tipe_bisnis', 'laundry')->get();
|
||||
|
||||
if ($kriteria->isEmpty()) {
|
||||
return redirect()->route('welcome')
|
||||
->with('error', 'Data kriteria laundry belum tersedia.');
|
||||
}
|
||||
|
||||
return view('user.preferensi-laundry', compact('kriteria'));
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error di User Preferensi Laundry: ' . $e->getMessage());
|
||||
return redirect()->route('welcome')->with('error', 'Terjadi kesalahan.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Halaman Search Laundry
|
||||
*/
|
||||
public function searchLaundry()
|
||||
{
|
||||
try {
|
||||
$kriteria = Kriteria::where('tipe_bisnis', 'laundry')->get();
|
||||
|
||||
if ($kriteria->isEmpty()) {
|
||||
return redirect()->route('welcome')
|
||||
->with('error', 'Data kriteria laundry belum tersedia.');
|
||||
}
|
||||
|
||||
return view('user.search-laundry', compact('kriteria'));
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error di User Search Laundry: ' . $e->getMessage());
|
||||
return redirect()->route('welcome')->with('error', 'Terjadi kesalahan.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Laundry dengan SAW (Auto - Tanpa Bobot Manual)
|
||||
*/
|
||||
public function calculateLaundry(Request $request)
|
||||
{
|
||||
try {
|
||||
// Ambil kriteria untuk laundry
|
||||
$kriteria = Kriteria::where('tipe_bisnis', 'laundry')->get();
|
||||
|
||||
if ($kriteria->isEmpty()) {
|
||||
return redirect()->route('user.search.laundry')
|
||||
->with('error', 'Data kriteria belum tersedia. Hubungi administrator.');
|
||||
}
|
||||
|
||||
// Validasi input hanya kategori dan lokasi
|
||||
$request->validate([
|
||||
'kategori_layanan' => 'required|in:reguler,express,kilat,premium',
|
||||
'user_lat' => 'nullable|numeric',
|
||||
'user_lng' => 'nullable|numeric',
|
||||
]);
|
||||
|
||||
// Ambil semua laundry
|
||||
$laundries = Laundry::all();
|
||||
|
||||
if ($laundries->isEmpty()) {
|
||||
return redirect()->route('user.search.laundry')
|
||||
->with('error', 'Belum ada data laundry yang tersedia.');
|
||||
}
|
||||
|
||||
// Tentukan lokasi referensi untuk perhitungan jarak
|
||||
$refLat = $request->user_lat ?: self::KAMPUS_LAT;
|
||||
$refLng = $request->user_lng ?: self::KAMPUS_LNG;
|
||||
$refName = ($request->user_lat && $request->user_lng) ? 'Lokasi Anda' : 'Kampus Polije';
|
||||
|
||||
Log::info('Laundry SAW Calculation (Auto):', [
|
||||
'kategori_layanan' => $request->kategori_layanan,
|
||||
'reference_location' => $refName,
|
||||
'ref_lat' => $refLat,
|
||||
'ref_lng' => $refLng,
|
||||
]);
|
||||
|
||||
// Hitung jarak dari lokasi referensi untuk setiap laundry
|
||||
foreach ($laundries as $laundry) {
|
||||
if (!empty($laundry->latitude) && !empty($laundry->longitude)) {
|
||||
$laundry->jarak_kampus = $this->haversineDistance(
|
||||
$refLat,
|
||||
$refLng,
|
||||
$laundry->latitude,
|
||||
$laundry->longitude
|
||||
);
|
||||
} else {
|
||||
$laundry->jarak_kampus = 999; // Default jarak besar jika koordinat tidak ada
|
||||
}
|
||||
}
|
||||
|
||||
// Proses Normalisasi dan Perhitungan SAW dengan filter kategori
|
||||
$hasil = $this->prosesLaundrySAW($laundries, $kriteria, $request);
|
||||
|
||||
// Simpan info untuk tampilan
|
||||
$bobotInfo = [
|
||||
'kategori_layanan' => $request->kategori_layanan,
|
||||
'reference_point' => $refName,
|
||||
'user_location' => ($request->user_lat && $request->user_lng) ? [
|
||||
'lat' => $request->user_lat,
|
||||
'lng' => $request->user_lng
|
||||
] : null,
|
||||
'mode' => 'auto'
|
||||
];
|
||||
|
||||
return view('user.hasil-laundry', compact('hasil', 'kriteria', 'bobotInfo'));
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error di Calculate Laundry: ' . $e->getMessage());
|
||||
return redirect()->route('user.search.laundry')
|
||||
->with('error', 'Terjadi kesalahan dalam perhitungan. Silakan coba lagi.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proses perhitungan SAW untuk Laundry (Auto - Bobot Seimbang)
|
||||
*/
|
||||
private function prosesLaundrySAW($laundries, $kriteria, $request)
|
||||
{
|
||||
$hasil = [];
|
||||
$normalizedData = [];
|
||||
$kategoriLayanan = $request->kategori_layanan;
|
||||
|
||||
// Filter laundry yang memiliki layanan sesuai kategori yang dipilih
|
||||
$laundries = $laundries->filter(function($laundry) use ($kategoriLayanan) {
|
||||
return $laundry->layanan()
|
||||
->where('jenis_layanan', $kategoriLayanan)
|
||||
->exists();
|
||||
});
|
||||
|
||||
if ($laundries->isEmpty()) {
|
||||
Log::warning('Tidak ada laundry dengan kategori: ' . $kategoriLayanan);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Step 1: Normalisasi
|
||||
foreach ($kriteria as $k) {
|
||||
$values = [];
|
||||
|
||||
// Kumpulkan nilai dengan key = laundry->id untuk menghindari undefined index
|
||||
foreach ($laundries as $laundry) {
|
||||
switch ($k->kode_kriteria) {
|
||||
case 'H': // Harga
|
||||
$values[$laundry->id] = $this->getHargaLaundryByKategori($laundry, $kategoriLayanan);
|
||||
break;
|
||||
case 'J': // Jarak
|
||||
$values[$laundry->id] = $laundry->jarak_kampus;
|
||||
break;
|
||||
case 'R': // Rating
|
||||
$values[$laundry->id] = $laundry->rating ?: 3; // Default 3 jika tidak ada rating
|
||||
break;
|
||||
case 'F': // Fasilitas
|
||||
$values[$laundry->id] = $this->getFasilitasScore($laundry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cek apakah array values tidak kosong sebelum memanggil max/min
|
||||
if (empty($values)) {
|
||||
continue; // Skip kriteria ini jika tidak ada nilai
|
||||
}
|
||||
|
||||
$max = max($values);
|
||||
$min = min($values);
|
||||
|
||||
foreach ($laundries as $laundry) {
|
||||
$value = $values[$laundry->id]; // Gunakan ID bukan index
|
||||
|
||||
if ($k->jenis_kriteria === 'benefit') {
|
||||
$normalizedData[$laundry->id][$k->kode_kriteria] = $max != $min ? $value / $max : 1;
|
||||
} else { // cost
|
||||
$normalizedData[$laundry->id][$k->kode_kriteria] = $min != $max ? $min / $value : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Perhitungan SAW dengan bobot seimbang otomatis
|
||||
$jumlahKriteria = $kriteria->count();
|
||||
$bobotSeimbang = $jumlahKriteria > 0 ? (1 / $jumlahKriteria) : 0.25; // Default 25% jika ada 4 kriteria
|
||||
|
||||
foreach ($laundries as $laundry) {
|
||||
$nilaiSAW = 0;
|
||||
|
||||
foreach ($kriteria as $k) {
|
||||
// Cek apakah ada data normalisasi untuk kriteria ini
|
||||
$nilaiNormalisasi = $normalizedData[$laundry->id][$k->kode_kriteria] ?? 0;
|
||||
$nilaiSAW += $bobotSeimbang * $nilaiNormalisasi;
|
||||
}
|
||||
|
||||
// Ambil layanan spesifik untuk kategori yang dipilih
|
||||
$layananSpesifik = $laundry->layanan()
|
||||
->where('jenis_layanan', $kategoriLayanan)
|
||||
->first();
|
||||
|
||||
$hasil[] = [
|
||||
'laundry' => $laundry,
|
||||
'layanan' => $layananSpesifik,
|
||||
'nilai' => round($nilaiSAW, 4),
|
||||
'detail' => $normalizedData[$laundry->id] ?? []
|
||||
];
|
||||
}
|
||||
|
||||
// Sort by nilai descending
|
||||
usort($hasil, function($a, $b) {
|
||||
return $b['nilai'] <=> $a['nilai'];
|
||||
});
|
||||
|
||||
return $hasil;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get harga rata-rata laundry
|
||||
*/
|
||||
private function getHargaLaundry($laundry)
|
||||
{
|
||||
// Ambil rata-rata harga layanan laundry
|
||||
$layanan = $laundry->layanan;
|
||||
if ($layanan->isEmpty()) {
|
||||
return 5000; // Default harga jika tidak ada layanan
|
||||
}
|
||||
|
||||
return $layanan->avg('harga') ?: 5000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get harga laundry berdasarkan kategori layanan
|
||||
*/
|
||||
private function getHargaLaundryByKategori($laundry, $kategoriLayanan)
|
||||
{
|
||||
// Ambil harga layanan laundry berdasarkan kategori
|
||||
$layanan = $laundry->layanan()
|
||||
->where('jenis_layanan', $kategoriLayanan)
|
||||
->first();
|
||||
|
||||
if (!$layanan) {
|
||||
return 999999; // Return nilai besar jika tidak ada layanan untuk kategori ini
|
||||
}
|
||||
|
||||
return $layanan->harga ?: 5000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fasilitas score berdasarkan jumlah fasilitas
|
||||
*/
|
||||
private function getFasilitasScore($laundry)
|
||||
{
|
||||
$fasilitas = $laundry->fasilitas ? explode(',', $laundry->fasilitas) : [];
|
||||
return count($fasilitas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance between two coordinates using Haversine formula
|
||||
*
|
||||
* @param float $lat1 Latitude of first point
|
||||
* @param float $lng1 Longitude of first point
|
||||
* @param float $lat2 Latitude of second point
|
||||
* @param float $lng2 Longitude of second point
|
||||
* @return float Distance in kilometers
|
||||
*/
|
||||
private function haversineDistance($lat1, $lng1, $lat2, $lng2)
|
||||
{
|
||||
$earthRadius = 6371; // Radius bumi dalam kilometer
|
||||
|
||||
$dLat = deg2rad($lat2 - $lat1);
|
||||
$dLng = deg2rad($lng2 - $lng1);
|
||||
|
||||
$a = sin($dLat / 2) * sin($dLat / 2) +
|
||||
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
|
||||
sin($dLng / 2) * sin($dLng / 2);
|
||||
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
|
||||
$distance = $earthRadius * $c; // Jarak dalam kilometer
|
||||
|
||||
return round($distance, 2); // Bulatkan 2 desimal
|
||||
}
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Kontrakan;
|
||||
use App\Models\Laundry;
|
||||
use App\Models\User;
|
||||
use App\Models\Review;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class WelcomeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display welcome page with real statistics
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// Cache data untuk 5 menit untuk performa yang lebih baik
|
||||
$stats = Cache::remember('welcome_stats', 300, function () {
|
||||
return [
|
||||
'jumlahKontrakan' => Kontrakan::count(),
|
||||
'jumlahLaundry' => Laundry::count(),
|
||||
'jumlahUsers' => User::count(),
|
||||
'totalReviews' => Review::count() ?? 0,
|
||||
];
|
||||
});
|
||||
|
||||
// Rekomendasi Kontrakan Terbaik (berdasarkan rating/harga)
|
||||
$topKontrakan = Cache::remember('top_kontrakan', 600, function () {
|
||||
return Kontrakan::select('id', 'nama', 'alamat', 'harga', 'jarak', 'luas', 'foto')
|
||||
->orderBy('luas', 'desc') // Prioritas luas besar
|
||||
->orderBy('harga', 'asc') // Harga murah
|
||||
->take(3)
|
||||
->get();
|
||||
});
|
||||
|
||||
// Rekomendasi Laundry Terbaik (berdasarkan harga termurah)
|
||||
$topLaundry = Cache::remember('top_laundry', 600, function () {
|
||||
return DB::table('laundry')
|
||||
->join(DB::raw('(
|
||||
SELECT laundry_id, MIN(harga) as min_harga
|
||||
FROM layanan_laundry
|
||||
GROUP BY laundry_id
|
||||
) as layanan'), 'laundry.id', '=', 'layanan.laundry_id')
|
||||
->select('laundry.*', 'layanan.min_harga')
|
||||
->orderBy('layanan.min_harga', 'asc')
|
||||
->take(3)
|
||||
->get();
|
||||
});
|
||||
|
||||
// Rating rata-rata (jika ada table reviews)
|
||||
$avgRating = Cache::remember('avg_rating', 600, function () {
|
||||
return Review::avg('rating') ?? 4.8;
|
||||
});
|
||||
|
||||
return view('welcome', compact(
|
||||
'stats',
|
||||
'topKontrakan',
|
||||
'topLaundry',
|
||||
'avgRating'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* API endpoint untuk mendapatkan statistik real-time
|
||||
*/
|
||||
public function getStats()
|
||||
{
|
||||
$stats = Cache::remember('api_welcome_stats', 60, function () {
|
||||
return [
|
||||
'jumlahKontrakan' => Kontrakan::count(),
|
||||
'jumlahLaundry' => Laundry::count(),
|
||||
'jumlahUsers' => User::count(),
|
||||
'totalReviews' => Review::count() ?? 0,
|
||||
'avgRating' => round(Review::avg('rating') ?? 4.8, 1),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show admin access page (separated from main homepage)
|
||||
*/
|
||||
public function adminAccess()
|
||||
{
|
||||
return view('admin.access');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class EnsureRole
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, string ...$roles)
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return redirect()->route('admin.login');
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if (!in_array($user->role, $roles, true)) {
|
||||
abort(403, 'Akses ditolak. Anda tidak memiliki izin untuk halaman ini.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
// Alias middleware admin
|
||||
$middleware->alias([
|
||||
'auth.admin' => \App\Http\Middleware\AdminAuth::class,
|
||||
'role' => \App\Http\Middleware\EnsureRole::class,
|
||||
]);
|
||||
|
||||
// Redirect guest ke admin.login
|
||||
|
|
|
|||
|
|
@ -272,8 +272,8 @@
|
|||
<a href="{{ route('admin.login') }}" class="btn-portal-primary">
|
||||
🔐 Login Pemilik
|
||||
</a>
|
||||
<a href="{{ route('admin.register') }}" class="btn-portal-secondary">
|
||||
📝 Daftar Sebagai Pemilik
|
||||
<a href="{{ route('admin.login') }}" class="btn-portal-secondary">
|
||||
ℹ️ Hubungi Super Admin
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -379,10 +379,10 @@
|
|||
<div style="max-width: 800px; margin: 0 auto;">
|
||||
<h2 class="cta-title">Siap Memulai?</h2>
|
||||
<p class="cta-subtitle">
|
||||
Daftar sekarang dan dapatkan akses penuh ke dashboard manajemen properti
|
||||
Hubungi super admin untuk mendapatkan akun dan akses dashboard
|
||||
</p>
|
||||
<a href="{{ route('admin.register') }}" class="btn-portal-primary" style="display: inline-block;">
|
||||
Daftar Gratis Sekarang
|
||||
<a href="{{ route('admin.login') }}" class="btn-portal-primary" style="display: inline-block;">
|
||||
Buka Halaman Login
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Activity Log Dashboard')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid px-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="mb-0">Dashboard Activity Log</h4>
|
||||
<a href="{{ route('admin.activity-logs.index') }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Kembali
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info border-0">
|
||||
Halaman ringkasan aktivitas tersedia. Gunakan halaman ini untuk melihat statistik aktivitas sistem.
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<small class="text-muted d-block">Total Aktivitas</small>
|
||||
<h5 class="mb-0">{{ $stats['total_logs'] ?? 0 }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<small class="text-muted d-block">Hari Ini</small>
|
||||
<h5 class="mb-0">{{ $stats['today_logs'] ?? 0 }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<small class="text-muted d-block">7 Hari</small>
|
||||
<h5 class="mb-0">{{ $stats['week_logs'] ?? 0 }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<small class="text-muted d-block">30 Hari</small>
|
||||
<h5 class="mb-0">{{ $stats['month_logs'] ?? 0 }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Security Monitoring')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid px-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="mb-0">Security Monitoring</h4>
|
||||
<a href="{{ route('admin.activity-logs.index') }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Kembali
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning border-0 mb-4">
|
||||
Gunakan halaman ini untuk memonitor aktivitas mencurigakan dan anomali penggunaan.
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0">
|
||||
<strong>Suspicious Activity</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0">Jumlah data: {{ $suspiciousActivity->count() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0">
|
||||
<strong>Failed Actions</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0">Jumlah data: {{ $failedActions->count() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Detail Activity Log')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid px-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb bg-transparent p-0">
|
||||
<li class="breadcrumb-item"><a href="{{ route('dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.activity-logs.index') }}">Activity Log</a></li>
|
||||
<li class="breadcrumb-item active">Detail</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
<i class="bi bi-clock-history me-2"></i>Detail Aktivitas
|
||||
</h5>
|
||||
<a href="{{ route('admin.activity-logs.index') }}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Kembali
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted mb-1">Waktu</label>
|
||||
<div class="fw-semibold">{{ $activityLog->created_at->format('d M Y H:i:s') }}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted mb-1">User</label>
|
||||
<div class="fw-semibold">{{ $activityLog->user->name ?? 'Unknown' }}</div>
|
||||
<small class="text-muted">{{ $activityLog->user->email ?? '-' }}</small>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted mb-1">Action</label>
|
||||
<div class="fw-semibold text-uppercase">{{ $activityLog->action }}</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted mb-1">Model Type</label>
|
||||
<div class="fw-semibold">{{ $activityLog->model_type ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted mb-1">Model ID</label>
|
||||
<div class="fw-semibold">{{ $activityLog->model_id ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label text-muted mb-1">Deskripsi</label>
|
||||
<div class="p-3 rounded border bg-light">{{ $activityLog->description }}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted mb-1">IP Address</label>
|
||||
<div class="fw-semibold">{{ $activityLog->ip_address ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted mb-1">User Agent</label>
|
||||
<div class="fw-semibold text-break">{{ $activityLog->user_agent ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($relatedModel)
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h6 class="mb-0 fw-semibold">
|
||||
<i class="bi bi-diagram-3 me-2"></i>Data Terkait
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted mb-1">Class</label>
|
||||
<div class="fw-semibold">{{ get_class($relatedModel) }}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted mb-1">ID</label>
|
||||
<div class="fw-semibold">{{ $relatedModel->id ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -172,7 +172,7 @@
|
|||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>Belum punya akun bisnis? <a href="{{ route('admin.register') }}">Daftar Sebagai Pemilik</a></p>
|
||||
<p>Belum punya akun bisnis? Hubungi super admin untuk pembuatan akun.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Tambah Review')
|
||||
|
||||
@section('content')
|
||||
<div class="container py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-7">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Tambah Review {{ ucfirst($type) }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ $type === 'kontrakan' ? route('reviews.kontrakan.store', $item->id) : route('reviews.laundry.store', $item->id) }}">
|
||||
@csrf
|
||||
<div class="mb-3">
|
||||
<label for="rating" class="form-label">Rating</label>
|
||||
<select name="rating" id="rating" class="form-select" required>
|
||||
<option value="">Pilih rating</option>
|
||||
@for($i = 5; $i >= 1; $i--)
|
||||
<option value="{{ $i }}" {{ old('rating') == $i ? 'selected' : '' }}>{{ $i }}</option>
|
||||
@endfor
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="review" class="form-label">Review</label>
|
||||
<textarea name="review" id="review" rows="4" class="form-control" placeholder="Tulis ulasan Anda...">{{ old('review') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Simpan Review</button>
|
||||
<a href="{{ url()->previous() }}" class="btn btn-outline-secondary">Batal</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Detail Kontrakan')
|
||||
|
||||
@section('content')
|
||||
<div class="container py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="mb-0">{{ $kontrakan->nama }}</h3>
|
||||
<a href="{{ route('kontrakan.index') }}" class="btn btn-outline-secondary btn-sm">Kembali</a>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<p class="mb-2"><strong>Alamat:</strong> {{ $kontrakan->alamat }}</p>
|
||||
<p class="mb-2"><strong>Harga:</strong> Rp {{ number_format($kontrakan->harga, 0, ',', '.') }}</p>
|
||||
<p class="mb-2"><strong>Jarak:</strong> {{ $kontrakan->jarak }} meter</p>
|
||||
<p class="mb-2"><strong>Jumlah Kamar:</strong> {{ $kontrakan->jumlah_kamar }}</p>
|
||||
@if(!empty($kontrakan->fasilitas))
|
||||
<p class="mb-0"><strong>Fasilitas:</strong> {{ $kontrakan->fasilitas }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -32,10 +32,7 @@
|
|||
// AUTH ADMIN (PUBLIC - TIDAK PERLU LOGIN)
|
||||
// -------------------------------------------------
|
||||
Route::get('/admin/login', [AdminAuthController::class, 'loginPage'])->name('admin.login');
|
||||
Route::post('/admin/login', [AdminAuthController::class, 'login'])->name('admin.login.post');
|
||||
|
||||
Route::get('/admin/register', [AdminAuthController::class, 'registerPage'])->name('admin.register');
|
||||
Route::post('/admin/register', [AdminAuthController::class, 'register'])->name('admin.register.post');
|
||||
Route::post('/admin/login', [AdminAuthController::class, 'login'])->middleware('throttle:10,1')->name('admin.login.post');
|
||||
|
||||
// -------------------------------------------------
|
||||
// ROUTE ADMIN TERPROTEKSI LOGIN
|
||||
|
|
@ -46,7 +43,7 @@
|
|||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||
|
||||
// ========== 🆕 CLEAR DASHBOARD CACHE (BARU!) ==========
|
||||
Route::get('/dashboard/clear-cache', [DashboardController::class, 'clearCache'])->name('dashboard.clear-cache');
|
||||
Route::post('/dashboard/clear-cache', [DashboardController::class, 'clearCache'])->name('dashboard.clear-cache');
|
||||
|
||||
// ========== 🆕 EXPORT ROUTES ==========
|
||||
Route::prefix('export')->name('export.')->group(function () {
|
||||
|
|
@ -64,21 +61,25 @@
|
|||
});
|
||||
|
||||
// ========== 🆕 ACTIVITY LOG ROUTES ==========
|
||||
Route::prefix('admin')->name('admin.')->group(function () {
|
||||
Route::prefix('admin')->name('admin.')->middleware('role:super_admin')->group(function () {
|
||||
Route::get('/activity-logs/dashboard', [ActivityLogController::class, 'dashboard'])->name('activity-logs.dashboard');
|
||||
Route::get('/activity-logs/security', [ActivityLogController::class, 'security'])->name('activity-logs.security');
|
||||
Route::get('/activity-logs/api-stats', [ActivityLogController::class, 'apiStats'])->name('activity-logs.api-stats');
|
||||
Route::post('/activity-logs/archive', [ActivityLogController::class, 'archive'])->name('activity-logs.archive');
|
||||
Route::get('/activity-logs/export', [ActivityLogController::class, 'export'])->name('activity-logs.export');
|
||||
Route::post('/activity-logs/clear', [ActivityLogController::class, 'clear'])->name('activity-logs.clear');
|
||||
Route::resource('activity-logs', ActivityLogController::class)->only(['index', 'show']);
|
||||
});
|
||||
|
||||
// ========== 🆕 USER MANAGEMENT ROUTES ==========
|
||||
Route::prefix('admin')->name('admin.')->group(function () {
|
||||
Route::prefix('admin')->name('admin.')->middleware('role:super_admin')->group(function () {
|
||||
Route::resource('users', UserManagementController::class);
|
||||
Route::post('/users/{user}/restore', [UserManagementController::class, 'restore'])->name('users.restore')->withTrashed();
|
||||
Route::post('/users/bulk-delete', [UserManagementController::class, 'bulkDelete'])->name('users.bulk-delete');
|
||||
});
|
||||
|
||||
// ========== 🆕 BACKUP & RESTORE ROUTES ==========
|
||||
Route::prefix('admin/backup')->name('admin.backup.')->group(function () {
|
||||
Route::prefix('admin/backup')->name('admin.backup.')->middleware('role:super_admin')->group(function () {
|
||||
Route::get('/', [BackupController::class, 'index'])->name('index');
|
||||
Route::post('/create', [BackupController::class, 'create'])->name('create');
|
||||
Route::get('/download/{backup}', [BackupController::class, 'download'])->name('download');
|
||||
|
|
@ -155,6 +156,9 @@
|
|||
|
||||
// Review Routes
|
||||
Route::prefix('reviews')->name('reviews.')->group(function () {
|
||||
Route::get('/{type}/{id}/create', [ReviewController::class, 'create'])
|
||||
->where('type', 'kontrakan|laundry')
|
||||
->name('create');
|
||||
Route::post('/kontrakan/{kontrakan}', [ReviewController::class, 'storeKontrakan'])->name('kontrakan.store');
|
||||
Route::post('/laundry/{laundry}', [ReviewController::class, 'storeLaundry'])->name('laundry.store');
|
||||
Route::delete('/{review}', [ReviewController::class, 'destroy'])->name('destroy');
|
||||
|
|
@ -170,4 +174,10 @@
|
|||
Route::post('/favorite/{type}/{id}', [FavoriteController::class, 'toggle'])->name('favorite.toggle');
|
||||
|
||||
Route::post('/admin/logout', [AdminAuthController::class, 'logout'])->name('admin.logout');
|
||||
});
|
||||
|
||||
// Register admin hanya untuk super admin yang sudah login
|
||||
Route::middleware(['auth', 'role:super_admin'])->group(function () {
|
||||
Route::get('/admin/register', [AdminAuthController::class, 'registerPage'])->name('admin.register');
|
||||
Route::post('/admin/register', [AdminAuthController::class, 'register'])->name('admin.register.post');
|
||||
});
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsureRole;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminSecurityRoutesTest extends TestCase
|
||||
{
|
||||
public function test_guest_is_redirected_from_admin_register_page(): void
|
||||
{
|
||||
$this->get(route('admin.register'))
|
||||
->assertRedirect(route('admin.login'));
|
||||
}
|
||||
|
||||
public function test_ensure_role_redirects_guest_to_login(): void
|
||||
{
|
||||
Auth::shouldReceive('check')->once()->andReturn(false);
|
||||
|
||||
$middleware = new EnsureRole();
|
||||
$response = $middleware->handle(Request::create('/admin/register', 'GET'), fn () => response('ok'), 'super_admin');
|
||||
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
$this->assertSame(route('admin.login'), $response->headers->get('Location'));
|
||||
}
|
||||
|
||||
public function test_ensure_role_forbids_non_matching_role(): void
|
||||
{
|
||||
Auth::shouldReceive('check')->once()->andReturn(true);
|
||||
Auth::shouldReceive('user')->once()->andReturn((object) ['role' => 'admin']);
|
||||
|
||||
$this->expectException(HttpException::class);
|
||||
$this->expectExceptionMessage('Akses ditolak. Anda tidak memiliki izin untuk halaman ini.');
|
||||
|
||||
$middleware = new EnsureRole();
|
||||
$middleware->handle(Request::create('/admin/register', 'GET'), fn () => response('ok'), 'super_admin');
|
||||
}
|
||||
|
||||
public function test_ensure_role_allows_matching_role(): void
|
||||
{
|
||||
Auth::shouldReceive('check')->once()->andReturn(true);
|
||||
Auth::shouldReceive('user')->once()->andReturn((object) ['role' => 'super_admin']);
|
||||
|
||||
$middleware = new EnsureRole();
|
||||
$response = $middleware->handle(Request::create('/admin/register', 'GET'), fn () => response('ok'), 'super_admin');
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertSame('ok', $response->getContent());
|
||||
}
|
||||
|
||||
public function test_guest_is_redirected_from_sensitive_admin_routes(): void
|
||||
{
|
||||
$this->get(route('admin.users.index'))->assertRedirect(route('admin.login'));
|
||||
$this->get(route('admin.backup.index'))->assertRedirect(route('admin.login'));
|
||||
$this->get(route('admin.activity-logs.index'))->assertRedirect(route('admin.login'));
|
||||
}
|
||||
|
||||
public function test_dashboard_clear_cache_rejects_get_method(): void
|
||||
{
|
||||
$this->get('/dashboard/clear-cache')->assertStatus(405);
|
||||
}
|
||||
|
||||
public function test_admin_login_post_has_throttle_middleware(): void
|
||||
{
|
||||
$route = Route::getRoutes()->getByName('admin.login.post');
|
||||
$middlewares = $route->gatherMiddleware();
|
||||
|
||||
$this->assertTrue(
|
||||
collect($middlewares)->contains(fn (string $item) => str_contains($item, 'throttle:10,1') || str_contains($item, 'ThrottleRequests:10,1')),
|
||||
'Route admin.login.post harus memiliki middleware throttle:10,1'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_admin_register_route_has_super_admin_role_middleware(): void
|
||||
{
|
||||
$route = Route::getRoutes()->getByName('admin.register');
|
||||
$middlewares = $route->gatherMiddleware();
|
||||
|
||||
$this->assertTrue(
|
||||
collect($middlewares)->contains(function (string $item) {
|
||||
return str_contains($item, 'role:super_admin')
|
||||
|| str_contains($item, EnsureRole::class.':super_admin');
|
||||
}),
|
||||
'Route admin.register harus dibatasi oleh middleware role:super_admin'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_sensitive_admin_routes_have_super_admin_role_middleware(): void
|
||||
{
|
||||
foreach ([
|
||||
'admin.users.index',
|
||||
'admin.backup.index',
|
||||
'admin.activity-logs.index',
|
||||
'admin.activity-logs.dashboard',
|
||||
'admin.activity-logs.security',
|
||||
'admin.activity-logs.archive',
|
||||
'admin.activity-logs.api-stats',
|
||||
] as $routeName) {
|
||||
$route = Route::getRoutes()->getByName($routeName);
|
||||
$middlewares = $route->gatherMiddleware();
|
||||
|
||||
$this->assertTrue(
|
||||
collect($middlewares)->contains(function (string $item) {
|
||||
return str_contains($item, 'role:super_admin')
|
||||
|| str_contains($item, EnsureRole::class.':super_admin');
|
||||
}),
|
||||
"Route {$routeName} harus dibatasi oleh middleware role:super_admin"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_review_create_route_is_registered(): void
|
||||
{
|
||||
$route = Route::getRoutes()->getByName('reviews.create');
|
||||
|
||||
$this->assertNotNull($route);
|
||||
$this->assertSame(['GET', 'HEAD'], $route->methods());
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,6 @@ public function test_the_application_returns_a_successful_response(): void
|
|||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertRedirect(route('admin.login'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue