343 lines
9.9 KiB
Markdown
343 lines
9.9 KiB
Markdown
# FIX: Hasil Rekomendasi Tidak Sesuai Fasilitas yang Dicentang
|
|
|
|
## 🐛 Masalah Yang Dilaporkan User
|
|
|
|
**Skenario**:
|
|
- User mengisi questionnaire: Budget termurah, Fasilitas: WiFi + AC + Parkir, Jarak: sedang
|
|
- User tekan "TEMUKAN KONTRAKAN"
|
|
- **Hasil yang muncul**: Kontrakan yang TIDAK punya fasilitas yang dipilih
|
|
- **Harapan**: Hanya kontrakan dengan SEMUA fasilitas yang dipilih
|
|
|
|
**Contoh**:
|
|
- User centang: Lemari, Kamar Mandi Luar, Dapur
|
|
- Hasil show: Kontrakan yang cuma punya "Lemari" saja (kurang 2 fasilitas)
|
|
|
|
---
|
|
|
|
## 🔍 Root Cause Analysis
|
|
|
|
### Problem Hierarchy
|
|
|
|
```
|
|
Issue: Hasil tidak sesuai dengan fasilitas yang dicentang
|
|
↓
|
|
Root Cause 1: Mobile app TIDAK mengirim selected_facilities ke API
|
|
- App collect selected facilities dari checkbox
|
|
- App calculate bobot dari questionnaire
|
|
- But: selected_facilities NOT in API request body
|
|
- Backend: Tidak tahu fasilitas mana yang user inginkan
|
|
- Result: Show ALL kontrakan (no facility filtering)
|
|
|
|
Root Cause 2: Backend tidak ada logic untuk filter by selected_facilities
|
|
- API validator: Accept 'fasilitas' string, but NOT 'selected_facilities' array
|
|
- SAW calculation: Cuma baca fasilitas_count (total jumlah)
|
|
- Filter logic: Missing untuk check "has all selected facilities"
|
|
```
|
|
|
|
---
|
|
|
|
## ✅ SOLUSI YANG DITERAPKAN
|
|
|
|
### Fix #1: Mobile App - Send Selected Facilities
|
|
|
|
**File**: `spk_mobile/lib/screens/recommendation_screen.dart` (Line 466-472)
|
|
|
|
**Perubahan**:
|
|
```dart
|
|
// BEFORE - Missing selected_facilities
|
|
final bodyParams = <String, dynamic>{};
|
|
bodyParams['bobot_harga'] = _bobotHarga;
|
|
bodyParams['bobot_jarak'] = _bobotJarak;
|
|
bodyParams['bobot_jumlah_kamar'] = _bobotKriteria3;
|
|
bodyParams['bobot_fasilitas'] = _bobotKriteria4;
|
|
// ❌ _selectedFacilities NOT sent!
|
|
|
|
// AFTER - Include selected_facilities array
|
|
final bodyParams = <String, dynamic>{};
|
|
bodyParams['bobot_harga'] = _bobotHarga;
|
|
bodyParams['bobot_jarak'] = _bobotJarak;
|
|
bodyParams['bobot_jumlah_kamar'] = _bobotKriteria3;
|
|
bodyParams['bobot_fasilitas'] = _bobotKriteria4;
|
|
// ✅ CRITICAL: Send selected facilities for filtering
|
|
bodyParams['selected_facilities'] = _selectedFacilities.toList();
|
|
```
|
|
|
|
**Impact**:
|
|
- Mobile app now sends: `{"selected_facilities": ["WiFi", "AC", "Parkir"], ...}`
|
|
- Backend knows exactly which facilities to filter
|
|
|
|
---
|
|
|
|
### Fix #2: Backend - Accept Selected Facilities
|
|
|
|
**File**: `spk_kontrakan/app/Http/Controllers/Api/SAWController.php` (Line 48-50)
|
|
|
|
**Perubahan Validator**:
|
|
```php
|
|
// BEFORE
|
|
'fasilitas' => 'nullable|string',
|
|
// (no selected_facilities parameter)
|
|
|
|
// AFTER
|
|
'fasilitas' => 'nullable|string',
|
|
'selected_facilities' => 'nullable|array', // Array dari questionnaire
|
|
'selected_facilities.*' => 'nullable|string',
|
|
```
|
|
|
|
**Impact**:
|
|
- Backend validator now accept array of selected facilities
|
|
- Allows: `POST /api/saw/calculate/kontrakan` dengan body `{selected_facilities: [...]}`
|
|
|
|
---
|
|
|
|
### Fix #3: Backend - Filter by Selected Facilities
|
|
|
|
**File**: `spk_kontrakan/app/Http/Controllers/Api/SAWController.php` (Line 135-156)
|
|
|
|
**Filtering Logic** (setelah `$kontrakan = $query->get()`):
|
|
|
|
```php
|
|
// Filter by selected_facilities from questionnaire
|
|
// Kontrakan HARUS punya SEMUA selected facilities
|
|
if ($request->has('selected_facilities') &&
|
|
is_array($request->selected_facilities) &&
|
|
!empty($request->selected_facilities)) {
|
|
|
|
$selectedFacilities = array_filter(array_map('trim', $request->selected_facilities));
|
|
|
|
if (!empty($selectedFacilities)) {
|
|
// Filter: kontrakan must have ALL selected facilities
|
|
$kontrakan = $kontrakan->filter(function($k) use ($selectedFacilities) {
|
|
$kontrakanFasilitas = array_map('trim', explode(',', $k->fasilitas));
|
|
|
|
// Check if kontrakan has ALL selected facilities
|
|
foreach ($selectedFacilities as $facility) {
|
|
if (!in_array($facility, $kontrakanFasilitas, true)) {
|
|
return false; // Missing this facility
|
|
}
|
|
}
|
|
return true; // Has all selected facilities
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**Logic**:
|
|
1. Take selected_facilities array from request
|
|
2. For each kontrakan, get list of facilities (split by comma)
|
|
3. Check: Does kontrakan have ALL selected facilities?
|
|
4. Keep only matching kontrakan
|
|
|
|
**Example**:
|
|
```
|
|
User selected: ["Lemari", "Kamar Mandi Luar", "Dapur"]
|
|
Kontrakan A has: "Lemari, Kamar Mandi Luar, Dapur, Tempat Cuci Piring" → ✓ KEEP
|
|
Kontrakan B has: "Lemari, Kamar Mandi Dalam" → ✗ REMOVE (missing 2)
|
|
Kontrakan C has: "Dapur, Parkir, AC" → ✗ REMOVE (missing 2)
|
|
```
|
|
|
|
---
|
|
|
|
## 🧪 TESTING RESULTS
|
|
|
|
### Automated Testing Script: `test_selected_facilities.php`
|
|
|
|
```
|
|
=== Test: Selected Facilities Filtering ===
|
|
|
|
Sample Kontrakan: kontrakan bangka
|
|
Facilities: Lemari, Kamar Mandi Luar, Dapur, ... (11 total)
|
|
|
|
--- Test Case 1: WITHOUT selected_facilities ---
|
|
Results count: 9 ✓
|
|
(Returns ALL kontrakan, ranked by SAW)
|
|
|
|
--- Test Case 2: WITH selected_facilities = ["Lemari", "Kamar Mandi Luar", "Dapur"] ---
|
|
Results count: 1 ✓
|
|
- kontrakan bangka (score: 1)
|
|
(Only kontrakan with ALL 3 facilities)
|
|
|
|
--- Test Case 3: WITH impossible facilities ---
|
|
Results count: 0 ✓
|
|
Message: Tidak ada kontrakan yang memenuhi kriteria yang Anda pilih
|
|
(Correctly returns error when no match)
|
|
```
|
|
|
|
### What Each Test Verifies
|
|
|
|
| Test | Input | Expected | Actual | Status |
|
|
|------|-------|----------|--------|--------|
|
|
| No filter | bobot only | 9 results (all) | 9 results | ✅ |
|
|
| Specific filter | 3 facilities | 1 result (kontrakan bangka) | 1 result | ✅ |
|
|
| Impossible filter | non-existent facilities | 0 results | 0 results | ✅ |
|
|
|
|
---
|
|
|
|
## 🔄 COMPLETE FLOW (BEFORE vs AFTER)
|
|
|
|
### BEFORE (Broken)
|
|
```
|
|
1. User fill questionnaire
|
|
- Budget: termurah
|
|
- Facilities: WiFi, AC, Parkir
|
|
- Jarak: sedang
|
|
- Kamar: tidak_peduli
|
|
|
|
2. User press "TEMUKAN KONTRAKAN"
|
|
|
|
3. Mobile app calculate bobot: (70, 10, 0, 20)
|
|
|
|
4. Mobile app calls API with:
|
|
POST /api/saw/calculate/kontrakan
|
|
{
|
|
"bobot_harga": 70,
|
|
"bobot_jarak": 10,
|
|
"bobot_jumlah_kamar": 0,
|
|
"bobot_fasilitas": 20
|
|
// ❌ selected_facilities NOT sent!
|
|
}
|
|
|
|
5. Backend SAW calculation:
|
|
- For ALL 9 kontrakan (no facility filter)
|
|
- Calculate scores based on harga, jarak, kamar, fasilitas_count
|
|
- Return ranked list
|
|
|
|
6. Result: Show kontrakan yang tidak punya WiFi+AC+Parkir
|
|
Example: Show kontrakan dengan AC saja ❌
|
|
```
|
|
|
|
### AFTER (Fixed)
|
|
```
|
|
1. User fill questionnaire
|
|
- Budget: termurah
|
|
- Facilities: WiFi, AC, Parkir
|
|
- Jarak: sedang
|
|
- Kamar: tidak_peduli
|
|
|
|
2. User press "TEMUKAN KONTRAKAN"
|
|
|
|
3. Mobile app calculate bobot: (70, 10, 0, 20)
|
|
|
|
4. Mobile app calls API with:
|
|
POST /api/saw/calculate/kontrakan
|
|
{
|
|
"bobot_harga": 70,
|
|
"bobot_jarak": 10,
|
|
"bobot_jumlah_kamar": 0,
|
|
"bobot_fasilitas": 20,
|
|
"selected_facilities": ["WiFi", "AC", "Parkir"] // ✅ Now sent!
|
|
}
|
|
|
|
5. Backend SAW calculation:
|
|
- FIRST: Filter kontrakan yang punya ALL 3 facilities (WiFi+AC+Parkir)
|
|
- THEN: Calculate SAW scores only for filtered kontrakan
|
|
- Return ranked list (only matching facilities)
|
|
|
|
6. Result: Show HANYA kontrakan dengan WiFi+AC+Parkir ✅
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 FILES MODIFIED
|
|
|
|
### Backend (Laravel)
|
|
1. **spk_kontrakan/app/Http/Controllers/Api/SAWController.php**
|
|
- Line 48-50: Added validator for `selected_facilities` array
|
|
- Line 135-156: Added filtering logic by selected_facilities
|
|
|
|
### Frontend (Flutter)
|
|
2. **spk_mobile/lib/screens/recommendation_screen.dart**
|
|
- Line 466-472: Added `selected_facilities` to API request body
|
|
|
|
---
|
|
|
|
## 🚀 HOW TO TEST
|
|
|
|
### Test 1: Manual Testing on Mobile App
|
|
|
|
```
|
|
1. Open app → Rekomendasi Kontrakan
|
|
2. Questionnaire appears
|
|
3. Q1: Select "Harga termurah"
|
|
4. Q2: Check SPECIFIC facilities (e.g., "Lemari", "Kamar Mandi Luar", "Dapur")
|
|
5. Q3: Select "Jarak sedang"
|
|
6. Q4: Select "Tidak peduli"
|
|
7. Press "TEMUKAN KONTRAKAN"
|
|
|
|
Expected:
|
|
- Results show ONLY kontrakan that have ALL selected facilities
|
|
- Each result should have all 3 checked facilities in their description
|
|
```
|
|
|
|
### Test 2: API Testing
|
|
|
|
```bash
|
|
cd c:\laragon\www\TA\spk_kontrakan
|
|
|
|
# Test with selected_facilities
|
|
php -r "
|
|
\$ch = curl_init('http://192.168.18.16:8000/api/saw/calculate/kontrakan');
|
|
curl_setopt(\$ch, CURLOPT_POST, 1);
|
|
curl_setopt(\$ch, CURLOPT_POSTFIELDS, json_encode([
|
|
'bobot_harga' => 70,
|
|
'bobot_jarak' => 10,
|
|
'bobot_jumlah_kamar' => 0,
|
|
'bobot_fasilitas' => 20,
|
|
'selected_facilities' => ['Lemari', 'Kamar Mandi Luar', 'Dapur']
|
|
]));
|
|
curl_setopt(\$ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
curl_setopt(\$ch, CURLOPT_RETURNTRANSFER, 1);
|
|
echo curl_exec(\$ch);
|
|
"
|
|
```
|
|
|
|
Expected result: Only kontrakan with all 3 facilities returned
|
|
|
|
---
|
|
|
|
## ✨ VERIFICATION CHECKLIST
|
|
|
|
- ✅ Mobile app sends `selected_facilities` array to API
|
|
- ✅ Backend validator accepts `selected_facilities` parameter
|
|
- ✅ Backend filtering logic correctly checks for ALL facilities
|
|
- ✅ API returns only matching kontrakan
|
|
- ✅ No facilities selected = all kontrakan returned (no filter)
|
|
- ✅ Impossible facilities selected = 0 results + error message
|
|
- ✅ Mixed edge cases handled correctly
|
|
|
|
---
|
|
|
|
## 📊 SUMMARY
|
|
|
|
| Aspect | BEFORE | AFTER |
|
|
|--------|--------|-------|
|
|
| Facilities sent to API | ❌ No | ✅ Yes (array) |
|
|
| Backend filtering | ❌ None | ✅ Full implementation |
|
|
| Results matching facilities | ❌ No | ✅ Yes (ALL selected) |
|
|
| Edge case: no filter | ✅ All returned | ✅ All returned |
|
|
| Edge case: impossible filter | ❌ All returned | ✅ 0 results + message |
|
|
|
|
---
|
|
|
|
## 🎯 CONCLUSION
|
|
|
|
**Problem**: User centang fasilitas tapi hasil tidak sesuai
|
|
|
|
**Root Cause**:
|
|
1. Mobile app tidak kirim selected_facilities ke API
|
|
2. Backend tidak ada filtering logic
|
|
|
|
**Solution Applied**:
|
|
1. Mobile: Add `selected_facilities` array to request
|
|
2. Backend: Accept dan implement filtering
|
|
|
|
**Result**: ✅ Results now match EXACTLY what user selected
|
|
|
|
**Status**: **RESOLVED** 🎉
|
|
|
|
---
|
|
|
|
**Last Updated**: May 19, 2026
|
|
**Tested**: ✓ All test cases passing
|
|
**Ready**: Mobile app + Backend API both updated and verified
|