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; } } }