get()->groupBy(function ($item) { return $item->kriteria->nama_kriteria; }); $pakaians = Pakaian::with('subKriterias')->get(); return view('landingpage.master', [ 'subKriteria' => $subKriteria, 'pakaians' => $pakaians ]); } public function prosesRekomendasi(Request $request) { // Input validation and processing $userInput = $this->processUserInput($request->input('sub_kriteria', [])); if (empty($userInput)) { return $this->returnNoResults('Silakan pilih minimal satu kriteria.'); } // \Log::debug('User Input:', $userInput); // Get selected subcriteria data $selectedSubkriteria = SubKriteria::whereIn('id', collect($userInput)->flatten()) ->get() ->keyBy('id'); // Extract criteria inputs $criteriaInputs = $this->extractCriteriaInputs($userInput, $selectedSubkriteria); // Apply strict filtering $filteredClothing = $this->applyStrictFiltering($criteriaInputs); if ($filteredClothing->isEmpty()) { return $this->returnNoResults('Tidak ada pakaian yang sesuai dengan kriteria yang Anda pilih.'); } // Calculate recommendations using weighted scoring $recommendations = $this->calculateRecommendations($filteredClothing, $userInput); return view('landingpage.hasil', compact('recommendations')); } /** * Process and validate user input */ private function processUserInput(array $userInput): array { $processedInput = []; foreach ($userInput as $kriteria_id => $subkriteria) { $subkriteriaArray = (array) $subkriteria; // Remove empty values and validate $cleanedSubkriteria = array_filter($subkriteriaArray, function($value) { return !empty($value) && is_numeric($value); }); if (!empty($cleanedSubkriteria)) { $processedInput[$kriteria_id] = array_map('intval', $cleanedSubkriteria); } } return $processedInput; } /** * Extract and organize criteria inputs */ private function extractCriteriaInputs(array $userInput, $selectedSubkriteria): array { $inputs = [ 'warna' => $userInput[1] ?? [], // Criteria 1: Color 'harga' => $userInput[2] ?? [], // Criteria 2: Price 'jenis_pakaian' => $userInput[3] ?? [], // Criteria 3: Clothing Type 'jenis_acara' => $userInput[4] ?? [], // Criteria 4: Event Type 'lokasi' => $userInput[5] ?? [] // Criteria 5: Location ]; // Calculate price range if price criteria is selected if (!empty($inputs['harga'])) { $hargaRanges = $selectedSubkriteria->only($inputs['harga']); $inputs['harga_min'] = $hargaRanges->min('min_value'); $inputs['harga_max'] = $hargaRanges->max('max_value'); } // \Log::debug("Extracted Criteria Inputs:", $inputs); return $inputs; } /** * Apply strict filtering based on mandatory criteria */ private function applyStrictFiltering(array $criteriaInputs) { $query = Pakaian::with(['penilaian.subkriteria.kriteria']); // Mandatory filter: Clothing Type (highest weight criteria) if (!empty($criteriaInputs['jenis_pakaian'])) { // \Log::debug('Applying clothing type filter:', $criteriaInputs['jenis_pakaian']); $query->whereHas('penilaian.subkriteria', function ($q) use ($criteriaInputs) { $q->where('kriteria_id', 3) ->whereIn('id', $criteriaInputs['jenis_pakaian']); }); } // Mandatory filter: Price Range (if specified) if (isset($criteriaInputs['harga_min']) && isset($criteriaInputs['harga_max'])) { // \Log::debug("Applying price filter: {$criteriaInputs['harga_min']} - {$criteriaInputs['harga_max']}"); $query->whereHas('penilaian.subkriteria', function ($q) use ($criteriaInputs) { $q->where('kriteria_id', 2) ->where('min_value', '<=', $criteriaInputs['harga_max']) ->where('max_value', '>=', $criteriaInputs['harga_min']); }); } $filteredClothing = $query->get(); // Validate filtering results $this->validateFilterResults($filteredClothing, $criteriaInputs); // \Log::debug('Filtered clothing count: ' . $filteredClothing->count()); return $filteredClothing; } /** * Validate that filtering worked correctly */ private function validateFilterResults($clothing, array $criteriaInputs): void { if (!empty($criteriaInputs['jenis_pakaian'])) { foreach ($clothing as $item) { $clothingTypeAssessment = $item->penilaian ->where('subkriteria.kriteria_id', 3) ->first(); if (!$clothingTypeAssessment || !in_array($clothingTypeAssessment->sub_kriteria_id, $criteriaInputs['jenis_pakaian'])) { // \Log::error("❌ Filter validation failed for clothing: {$item->nama}"); } } } } /** * Calculate recommendations using SAW method */ private function calculateRecommendations($clothing, array $userInput): \Illuminate\Support\Collection { // Get criteria with weights $criteria = Kriteria::with('subkriteria') ->whereIn('id', [1, 2, 3, 4, 5]) ->orderByDesc('bobot') // Order by weight (highest first) ->get(); // Build decision matrix $decisionMatrix = $this->buildDecisionMatrix($clothing, $criteria, $userInput); // Calculate min/max values for normalization $normalizationValues = $this->calculateNormalizationValues($decisionMatrix, $criteria); // Calculate preference scores $results = $this->calculatePreferenceScores( $clothing, $decisionMatrix, $criteria, $normalizationValues, $userInput ); // Sort by score (descending) $recommendations = collect($results) ->sortByDesc('score') ->values(); $this->logFinalRecommendations($recommendations); return $recommendations; } /** * Build decision matrix for each clothing item */ private function buildDecisionMatrix($clothing, $criteria, array $userInput): array { $matrix = []; foreach ($clothing as $item) { $matrix[$item->id] = []; foreach ($criteria as $criterion) { $userSubIds = $userInput[$criterion->id] ?? []; if (empty($userSubIds)) { continue; // Skip criteria not selected by user } // Get matching assessments for this criterion $matchingAssessments = $item->penilaian->filter(function ($assessment) use ($userSubIds) { return in_array($assessment->sub_kriteria_id, $userSubIds); }); if ($matchingAssessments->isNotEmpty()) { // Use average if multiple matches, first match for clothing type $value = ($criterion->id == 3) ? $matchingAssessments->first()->nilai : $matchingAssessments->avg('nilai'); $matrix[$item->id][$criterion->id] = $value; // \Log::debug("Item {$item->id} - Criterion {$criterion->id}: {$value}"); } } } return $matrix; } /** * Calculate min/max values for normalization */ private function calculateNormalizationValues(array $decisionMatrix, $criteria): array { $values = ['max' => [], 'min' => []]; foreach ($criteria as $criterion) { $criterionValues = collect($decisionMatrix) ->pluck($criterion->id) ->filter(function($value) { return $value !== null && $value > 0; }); if ($criterionValues->isNotEmpty()) { $values['max'][$criterion->id] = $criterionValues->max(); $values['min'][$criterion->id] = $criterionValues->min(); } else { // Fallback values $values['max'][$criterion->id] = 1; $values['min'][$criterion->id] = 1; } // \Log::debug("Criterion {$criterion->id} normalization - Max: {$values['max'][$criterion->id]}, Min: {$values['min'][$criterion->id]}"); } return $values; } /** * Calculate final preference scores using SAW method */ private function calculatePreferenceScores($clothing, array $decisionMatrix, $criteria, array $normValues, array $userInput): array { $results = []; foreach ($clothing as $item) { $preferenceScore = 0; $totalWeight = 0; foreach ($criteria as $criterion) { $userSubIds = $userInput[$criterion->id] ?? []; if (empty($userSubIds) || !isset($decisionMatrix[$item->id][$criterion->id])) { continue; } $value = $decisionMatrix[$item->id][$criterion->id]; if ($value <= 0) continue; // Normalize value based on criterion type $normalizedValue = $this->normalizeValue( $value, $normValues['max'][$criterion->id], $normValues['min'][$criterion->id], $criterion->jenis ); // Calculate weighted contribution $weight = $criterion->bobot; $contribution = $normalizedValue * $weight; $preferenceScore += $contribution; $totalWeight += $weight; // \Log::debug("Item {$item->id} - Criterion {$criterion->id}: Value={$value}, Normalized={$normalizedValue}, Weight={$weight}, Contribution={$contribution}"); } $clothingType = $item->penilaian ->firstWhere('subkriteria.kriteria_id', 3) ?->subkriteria->nama ?? 'Lainnya'; $results[] = [ 'pakaian' => $item, 'score' => round($preferenceScore, 4), 'jenis_pakaian' => $clothingType, 'total_weight' => $totalWeight ]; // \Log::debug("📊 Final Score - Item {$item->id} ({$item->nama}): {$preferenceScore}"); } return $results; } /** * Normalize value based on criterion type (BENEFIT/COST) */ private function normalizeValue(float $value, float $max, float $min, string $type): float { if ($type === 'COST') { return $value > 0 ? $min / $value : 0; } else { return $max > 0 ? $value / $max : 0; } } /** * Log final recommendations */ private function logFinalRecommendations($recommendations): void { // \Log::debug('🏆 FINAL RECOMMENDATIONS:'); foreach ($recommendations as $index => $item) { $rank = $index + 1; // \Log::debug("{$rank}. {$item['pakaian']->nama} - Score: {$item['score']} - Type: {$item['jenis_pakaian']}"); } } /** * Return view with no results message */ private function returnNoResults(string $message) { return view('landingpage.hasil', [ 'recommendations' => collect([]), 'message' => $message ]); } }