MIF_E31230069/spk_kontrakan/app/Http/Controllers/SAWController.php

409 lines
16 KiB
PHP

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Kontrakan;
use App\Models\Laundry;
use App\Models\Kriteria;
use Illuminate\Support\Facades\Log;
use Exception;
class SAWController extends Controller
{
// Koordinat Kampus Polije (FIXED)
const KAMPUS_LAT = -8.15981; // Ganti dengan koordinat kampus Polije yang sebenarnya
const KAMPUS_LNG = 113.72312; // Ganti dengan koordinat kampus Polije yang sebenarnya
public function index()
{
try {
$kriteria = Kriteria::all();
if ($kriteria->isEmpty()) {
return view('saw.index')->with('error', 'Data kriteria belum tersedia. Hubungi administrator.');
}
return view('saw.index', compact('kriteria'));
} catch (Exception $e) {
Log::error('Error di SAW Index: ' . $e->getMessage());
return redirect()->back()->with('error', 'Terjadi kesalahan saat memuat halaman. Silakan coba lagi.');
}
}
// Halaman untuk setting bobot kriteria
public function bobot()
{
try {
$kriteriaKontrakan = Kriteria::where('tipe_bisnis', 'kontrakan')->get();
$kriteriaLaundry = Kriteria::where('tipe_bisnis', 'laundry')->get();
return view('saw.bobot', compact('kriteriaKontrakan', 'kriteriaLaundry'));
} catch (Exception $e) {
Log::error('Error di SAW Bobot: ' . $e->getMessage());
return redirect()->back()->with('error', 'Terjadi kesalahan. Silakan coba lagi.');
}
}
public function proses(Request $request)
{
try {
// Validasi input
$request->validate([
'tipe' => 'required|in:kontrakan,laundry',
'jenis_layanan' => 'required_if:tipe,laundry',
], [
'tipe.required' => 'Pilih tipe bisnis terlebih dahulu!',
'tipe.in' => 'Tipe bisnis tidak valid!',
'jenis_layanan.required_if' => 'Pilih jenis layanan untuk laundry!',
]);
$tipe = $request->tipe;
$jenisLayanan = $request->jenis_layanan ?? null;
// Default: gunakan koordinat KAMPUS
$userLat = self::KAMPUS_LAT;
$userLng = self::KAMPUS_LNG;
$referencePoint = 'Kampus Polije';
// Untuk LAUNDRY: cek apakah user memilih "dari lokasi saya" dan sudah deteksi lokasi
if ($tipe == 'laundry') {
$referensiJarak = $request->input('referensi_jarak', 'kampus');
if ($referensiJarak == 'user' && $request->filled('user_lat') && $request->filled('user_lng')) {
// User memilih "dari lokasi saya" DAN sudah deteksi lokasi
$userLat = floatval($request->user_lat);
$userLng = floatval($request->user_lng);
$referencePoint = 'Lokasi Anda';
Log::info('Laundry: Menggunakan lokasi USER', [
'lat' => $userLat,
'lng' => $userLng
]);
} else {
// Default atau user pilih "dari kampus" atau belum deteksi lokasi
Log::info('Laundry: Menggunakan lokasi KAMPUS (default)');
}
}
Log::info('SAW Input:', [
'tipe' => $tipe,
'jenis_layanan' => $jenisLayanan,
'reference_lat' => $userLat,
'reference_lng' => $userLng,
'reference_point' => $referencePoint,
]);
// Ambil kriteria
$kriteria = Kriteria::where('tipe_bisnis', $tipe)->get();
if ($kriteria->isEmpty()) {
return redirect()->route('saw.index')
->with('error', 'Kriteria untuk ' . $tipe . ' belum tersedia!');
}
// Validasi total bobot harus = 1
$totalBobot = $kriteria->sum('bobot');
if (abs($totalBobot - 1) > 0.01) {
Log::warning('Total bobot kriteria tidak = 1: ' . $totalBobot);
return redirect()->route('saw.index')
->with('error', 'Konfigurasi bobot kriteria tidak valid (total: ' . $totalBobot . '). Hubungi administrator.');
}
// Ambil data
$data = $this->getData($tipe, $jenisLayanan);
if ($data->isEmpty()) {
return redirect()->route('saw.index')
->with('error', 'Tidak ada data ' . $tipe . ' yang tersedia' .
($jenisLayanan ? ' untuk layanan ' . $jenisLayanan : '') . '!');
}
// Proses data dengan fasilitas/layanan dan hitung jarak
$dataWithFasilitas = $this->processData($data, $tipe, $jenisLayanan, $userLat, $userLng);
if ($dataWithFasilitas->isEmpty()) {
return redirect()->route('saw.index')
->with('error', 'Tidak ada data yang valid untuk diproses!');
}
// Hitung max/min
$maxMin = $this->calculateMaxMin($dataWithFasilitas, $tipe);
// Validasi max/min tidak nol
foreach ($maxMin as $key => $values) {
if ($values['max'] == 0 && $values['min'] == 0) {
Log::warning("Max/Min untuk {$key} adalah 0");
}
}
Log::info('Max/Min Values:', $maxMin);
// Normalisasi dan hitung
$hasil = $this->calculateSAW($dataWithFasilitas, $kriteria, $maxMin, $tipe);
if (empty($hasil)) {
return redirect()->route('saw.index')
->with('error', 'Gagal menghitung hasil. Silakan coba lagi.');
}
// Kirim info reference point ke view
$referencePoint = $referencePoint; // sudah didefinisikan di atas
return view('saw.hasil', compact('hasil', 'kriteria', 'tipe', 'jenisLayanan', 'userLat', 'userLng', 'referencePoint'));
} catch (\Illuminate\Validation\ValidationException $e) {
return redirect()->route('saw.index')
->withErrors($e->validator)
->withInput();
} catch (Exception $e) {
Log::error('Error di SAW Proses: ' . $e->getMessage(), [
'trace' => $e->getTraceAsString()
]);
return redirect()->route('saw.index')
->with('error', 'Terjadi kesalahan saat memproses data. Silakan coba lagi.');
}
}
// Ambil data sesuai tipe
private function getData($tipe, $jenisLayanan)
{
try {
if ($tipe == 'kontrakan') {
return Kontrakan::all();
}
$data = Laundry::with('layanan')->get();
$jenisValues = $this->mapLaundryJenisLayananValues($jenisLayanan);
return $data->filter(function($laundry) use ($jenisValues) {
return $laundry->layanan->whereIn('jenis_layanan', $jenisValues)->isNotEmpty();
});
} catch (Exception $e) {
Log::error('Error getData: ' . $e->getMessage());
throw new Exception('Gagal mengambil data dari database');
}
}
// Proses data dengan hitung fasilitas/jarak
private function processData($data, $tipe, $jenisLayanan, $refLat, $refLng)
{
$jenisValues = $this->mapLaundryJenisLayananValues($jenisLayanan);
return $data->map(function($item) use ($tipe, $jenisValues, $refLat, $refLng) {
try {
if ($tipe == 'kontrakan') {
$item->jumlah_fasilitas = $item->fasilitas ? count(explode(',', $item->fasilitas)) : 0;
$item->harga_value = $item->harga ?? 0;
// KONTRAKAN: Hitung jarak dari KAMPUS POLIJE
if ($item->latitude && $item->longitude) {
try {
$item->jarak_value = $item->calculateDistance($refLat, $refLng) * 1000; // ke meter
} catch (Exception $e) {
Log::warning("Gagal hitung jarak untuk kontrakan {$item->nama}: " . $e->getMessage());
$item->jarak_value = $item->jarak ?? 0;
}
} else {
$item->jarak_value = $item->jarak ?? 0;
}
$item->latitude_value = $item->latitude;
$item->longitude_value = $item->longitude;
} else {
// LAUNDRY
$layanan = $item->layanan->whereIn('jenis_layanan', $jenisValues)->first();
if (!$layanan) {
return null;
}
$item->harga_value = $layanan->harga ?? 0;
$item->kecepatan_value = $layanan->estimasi_selesai ?? 0; // sudah dalam satuan jam
// LAUNDRY: Hitung jarak dari LOKASI USER
if ($item->latitude && $item->longitude) {
try {
$item->jarak_value = $item->calculateDistance($refLat, $refLng) * 1000; // ke meter
} catch (Exception $e) {
Log::warning("Gagal hitung jarak untuk laundry {$item->nama}: " . $e->getMessage());
$item->jarak_value = $item->jarak ?? 0;
}
} else {
$item->jarak_value = $item->jarak ?? 0;
}
$item->jumlah_fasilitas = $item->layanan->count();
$item->latitude_value = $item->latitude;
$item->longitude_value = $item->longitude;
}
return $item;
} catch (Exception $e) {
Log::error("Error processing item {$item->id}: " . $e->getMessage());
return null;
}
})->filter();
}
// Mapping jenis layanan baru ke nilai lama agar kompatibel dengan data existing.
private function mapLaundryJenisLayananValues($jenisLayanan)
{
$map = [
'harian' => ['harian', 'reguler', 'kiloan'],
'jam' => ['jam', 'express', 'kilat', 'satuan'],
// Backward compatibility
'reguler' => ['harian', 'reguler', 'kiloan'],
'express' => ['jam', 'express', 'kilat', 'satuan'],
'kilat' => ['jam', 'express', 'kilat', 'satuan'],
];
return $map[$jenisLayanan] ?? [$jenisLayanan];
}
// Hitung max/min untuk setiap kriteria
private function calculateMaxMin($dataWithFasilitas, $tipe)
{
$maxMin = [];
$maxMin['harga'] = [
'max' => $dataWithFasilitas->max('harga_value') ?: 1,
'min' => $dataWithFasilitas->min('harga_value') ?: 1,
];
$maxMin['jarak'] = [
'max' => $dataWithFasilitas->max('jarak_value') ?: 1,
'min' => $dataWithFasilitas->min('jarak_value') ?: 1,
];
if ($tipe == 'kontrakan') {
$maxMin['jumlah_kamar'] = [
'max' => $dataWithFasilitas->max('jumlah_kamar') ?: 1,
'min' => $dataWithFasilitas->min('jumlah_kamar') ?: 1,
];
}
if ($tipe == 'laundry') {
$maxMin['kecepatan'] = [
'max' => $dataWithFasilitas->max('kecepatan_value') ?: 1,
'min' => $dataWithFasilitas->min('kecepatan_value') ?: 1,
];
}
$maxMin['fasilitas'] = [
'max' => $dataWithFasilitas->max('jumlah_fasilitas') ?: 1,
'min' => $dataWithFasilitas->min('jumlah_fasilitas') ?: 1,
];
return $maxMin;
}
// Hitung SAW
private function calculateSAW($dataWithFasilitas, $kriteria, $maxMin, $tipe)
{
$hasil = [];
foreach ($dataWithFasilitas as $item) {
try {
$normalisasi = [];
$nilaiTotal = 0;
foreach ($kriteria as $krit) {
$namaKrit = strtolower($krit->nama_kriteria);
$bobotKrit = $krit->bobot;
$tipeKrit = strtolower($krit->tipe);
$nilaiAsli = 0;
$nilaiNormalisasi = 0;
// Mapping kriteria
if (strpos($namaKrit, 'harga') !== false) {
$nilaiAsli = $item->harga_value;
$nilaiNormalisasi = $this->normalize($nilaiAsli, $maxMin['harga'], $tipeKrit);
} elseif (strpos($namaKrit, 'jarak') !== false) {
$nilaiAsli = $item->jarak_value;
$nilaiNormalisasi = $this->normalize($nilaiAsli, $maxMin['jarak'], $tipeKrit);
} elseif ((strpos($namaKrit, 'jumlah_kamar') !== false || strpos($namaKrit, 'jumlah kamar') !== false || strpos($namaKrit, 'luas') !== false) && $tipe == 'kontrakan') {
// Support backward compatibility dengan 'luas' tapi gunakan jumlah_kamar
$nilaiAsli = $item->jumlah_kamar ?? 0;
$nilaiNormalisasi = $this->normalize($nilaiAsli, $maxMin['jumlah_kamar'], $tipeKrit);
} elseif (strpos($namaKrit, 'fasilitas') !== false || strpos($namaKrit, 'layanan') !== false) {
$nilaiAsli = $item->jumlah_fasilitas;
$nilaiNormalisasi = $this->normalize($nilaiAsli, $maxMin['fasilitas'], $tipeKrit);
} elseif (strpos($namaKrit, 'kecepatan') !== false && $tipe == 'laundry') {
$nilaiAsli = $item->kecepatan_value;
$nilaiNormalisasi = $this->normalize($nilaiAsli, $maxMin['kecepatan'], $tipeKrit);
}
$normalisasi[$krit->nama_kriteria] = [
'asli' => $nilaiAsli,
'normalisasi' => round($nilaiNormalisasi, 4),
'bobot' => $bobotKrit,
];
$nilaiTotal += ($nilaiNormalisasi * $bobotKrit);
}
$hasil[] = [
'id' => $item->id,
'nama' => $item->nama,
'alamat' => $item->alamat,
'latitude' => $item->latitude_value,
'longitude' => $item->longitude_value,
'no_whatsapp' => $item->no_whatsapp ?? null,
'normalisasi' => $normalisasi,
'nilai' => round($nilaiTotal, 4),
];
} catch (Exception $e) {
Log::error("Error calculating SAW for item {$item->id}: " . $e->getMessage());
continue;
}
}
// Sort hasil
usort($hasil, fn($a, $b) => $b['nilai'] <=> $a['nilai']);
// Tambahkan ranking
foreach ($hasil as $key => $value) {
$hasil[$key]['ranking'] = $key + 1;
}
return $hasil;
}
// Helper normalisasi dengan error handling
private function normalize($nilai, $maxMin, $tipe)
{
try {
// Cegah division by zero
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;
}
}
}