Filter Kategori Mainan

This commit is contained in:
iFannJR 2026-06-03 00:27:58 +07:00
parent e68e59a169
commit 6eaf12ef7b
4 changed files with 191 additions and 76 deletions

View File

@ -6,17 +6,28 @@
use App\Models\Criteria;
use App\Services\ToyRecommendationService;
use Illuminate\Http\Request;
use App\Models\Product;
class RecommendationController extends Controller
{ #untuk mengelola proses rekomendasi mainan, termasuk menampilkan form rekomendasi dan menampilkan hasil rekomendasi berdasarkan input pengguna
public function __construct(
protected ToyRecommendationService $sawService
) {}
) {
}
public function index()
{
$criterias = Criteria::orderBy('weight_order')->get();
return view('user.recommendation', compact('criterias'));
$categories = Product::select('category')
->distinct()
->orderBy('category')
->pluck('category');
return view('user.recommendation', compact(
'criterias',
'categories'
));
}
public function result(RecommendationRequest $request)
@ -27,6 +38,7 @@ public function result(RecommendationRequest $request)
'budget_min' => $request->budget_min,
'budget_max' => $request->budget_max,
'priorities' => $request->input('priorities', []),
'category' => $request->category,
];
$data = $this->sawService->getRecommendationsWithMatrix($input);

View File

@ -20,6 +20,7 @@ public function rules(): array
'budget_max' => ['required', 'numeric', 'min:0', 'gte:budget_min'],
'priorities' => ['nullable', 'array'],
'priorities.*' => ['nullable', 'numeric', 'min:1', 'max:5'],
'category' => ['nullable', 'string'],
];
}
}

View File

@ -14,6 +14,51 @@ class ToyRecommendationService
* @param array $input ['age_min', 'age_max', 'budget_min', 'budget_max', 'priorities' => [criteria_id=>1-5, ...]]
* @return Collection<int, array{rank: int, product: Product, score: float, explanation: string}>
*/
// public function getRecommendations(array $input): Collection
// {
// $ageMin = (int) ($input['age_min'] ?? 0);
// $ageMax = (int) ($input['age_max'] ?? 99);
// $budgetMin = (float) ($input['budget_min'] ?? 0);
// $budgetMax = (float) ($input['budget_max'] ?? 999999999);
// $priorities = $input['priorities'] ?? [];
// $products = $this->getFilteredProducts($ageMin, $ageMax, $budgetMin, $budgetMax,);
// if ($products->isEmpty()) {
// return collect();
// }
// $criterias = Criteria::orderBy('weight_order')->get();
// if ($criterias->isEmpty()) {
// return $products->take(5)->map(fn($p, $i) => [
// 'rank' => $i + 1,
// 'product' => $p,
// 'score' => 0.0,
// 'explanation' => 'Produk ini tersedia dalam rentang usia dan budget Anda.',
// ]);
// }
// $weights = $this->buildWeightsFromPriorities($priorities, $criterias);
// $matrix = $this->buildMatrix($products, $criterias);
// $normalized = $this->normalizeMatrix($matrix, $criterias);
// $scores = $this->calculatePreferenceScores($normalized, $weights, $criterias);
// $ranked = $scores->sortByDesc('score')->take(5)->values();
// $criteriaNames = $criterias->keyBy('id')->map->name;
// return $ranked->map(function ($item, $index) use ($products, $criteriaNames, $priorities) {
// $product = $products->firstWhere('id', $item['product_id']);
// if (!$product) {
// return null;
// }
// $explanation = $this->buildExplanation($product, $item['score'], $priorities, $criteriaNames);
// return [
// 'rank' => $index + 1,
// 'product' => $product,
// 'score' => round($item['score'], 4),
// 'explanation' => $explanation,
// ];
// })->filter()->values();
// }
public function getRecommendations(array $input): Collection
{
$ageMin = (int) ($input['age_min'] ?? 0);
@ -22,12 +67,22 @@ public function getRecommendations(array $input): Collection
$budgetMax = (float) ($input['budget_max'] ?? 999999999);
$priorities = $input['priorities'] ?? [];
$products = $this->getFilteredProducts($ageMin, $ageMax, $budgetMin, $budgetMax);
$category = $input['category'] ?? null;
$products = $this->getFilteredProducts(
$ageMin,
$ageMax,
$budgetMin,
$budgetMax,
$category
);
if ($products->isEmpty()) {
return collect();
}
$criterias = Criteria::orderBy('weight_order')->get();
if ($criterias->isEmpty()) {
return $products->take(5)->map(fn($p, $i) => [
'rank' => $i + 1,
@ -47,10 +102,18 @@ public function getRecommendations(array $input): Collection
return $ranked->map(function ($item, $index) use ($products, $criteriaNames, $priorities) {
$product = $products->firstWhere('id', $item['product_id']);
if (!$product) {
return null;
}
$explanation = $this->buildExplanation($product, $item['score'], $priorities, $criteriaNames);
$explanation = $this->buildExplanation(
$product,
$item['score'],
$priorities,
$criteriaNames
);
return [
'rank' => $index + 1,
'product' => $product,
@ -59,7 +122,6 @@ public function getRecommendations(array $input): Collection
];
})->filter()->values();
}
/**
* Hitung rekomendasi SAW beserta data matriks untuk tampilan (skripsi).
*
@ -73,7 +135,16 @@ public function getRecommendationsWithMatrix(array $input): array
$budgetMax = (float) ($input['budget_max'] ?? 999999999);
$priorities = $input['priorities'] ?? [];
$products = $this->getFilteredProducts($ageMin, $ageMax, $budgetMin, $budgetMax);
$category = $input['category'] ?? null;
$products = $this->getFilteredProducts(
$ageMin,
$ageMax,
$budgetMin,
$budgetMax,
$category
);
if ($products->isEmpty()) {
return [
'recommendations' => collect(),
@ -178,17 +249,35 @@ public function getRecommendationsWithMatrix(array $input): array
];
}
protected function getFilteredProducts(int $ageMin, int $ageMax, float $budgetMin, float $budgetMax): Collection
{
return Product::with('criterias')
protected function getFilteredProducts(
int $ageMin,
int $ageMax,
float $budgetMin,
float $budgetMax,
?string $category = null
): Collection
{
$query = Product::with('criterias')
->where('stock', '>', 0)
->whereBetween('price', [$budgetMin, $budgetMax])
->get()
->whereBetween('price', [$budgetMin, $budgetMax]);
// Filter kategori jika dipilih
if (!empty($category)) {
$query->where('category', $category);
}
return $query->get()
->filter(function ($product) use ($ageMin, $ageMax) {
if (empty($product->age_range)) {
return true;
}
return $this->ageRangeOverlaps($product->age_range, $ageMin, $ageMax);
return $this->ageRangeOverlaps(
$product->age_range,
$ageMin,
$ageMax
);
})
->values();
}

View File

@ -3,71 +3,84 @@
@section('title', 'Konsultasi Rekomendasi SAW')
<!-- Halaman Konsultasi -->
@section('content')
<div class="max-w-3xl mx-auto space-y-8">
<div>
<h2 class="text-2xl font-bold text-gray-900 tracking-tight">Konsultasi Rekomendasi</h2>
<p class="mt-1 text-base text-gray-500">Isi profil anak dan prioritas kriteria. Sistem akan merekomendasikan mainan terbaik dengan metode SAW.</p>
</div>
<div class="max-w-3xl mx-auto space-y-8">
<div>
<h2 class="text-2xl font-bold text-gray-900 tracking-tight">Konsultasi Rekomendasi</h2>
<p class="mt-1 text-base text-gray-500">Isi profil anak dan prioritas kriteria. Sistem akan merekomendasikan
mainan terbaik dengan metode SAW.</p>
</div>
<form method="POST" action="{{ route('recommendation.result') }}" class="space-y-6">
@csrf
<form method="POST" action="{{ route('recommendation.result') }}" class="space-y-6">
@csrf
{{-- Step 1: Profil anak --}}
<x-card>
<h3 class="text-xl font-semibold text-gray-900 mb-1">Profil anak</h3>
<p class="text-sm text-gray-500 mb-6">Rentang usia dan budget yang Anda inginkan.</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<x-input label="Usia minimal (tahun)" name="age_min" type="number" value="{{ old('age_min', 3) }}" min="0" max="18" required />
<x-input label="Usia maksimal (tahun)" name="age_max" type="number" value="{{ old('age_max', 6) }}" min="0" max="18" required />
<x-input label="Budget minimal (Rp)" name="budget_min" type="number" value="{{ old('budget_min', 50000) }}" min="0" required />
<x-input label="Budget maksimal (Rp)" name="budget_max" type="number" value="{{ old('budget_max', 300000) }}" min="0" required />
</div>
</x-card>
{{-- Step 2: Prioritas kriteria --}}
<x-card>
<h3 class="text-xl font-semibold text-gray-900 mb-1">Prioritas kriteria</h3>
<p class="text-sm text-gray-500 mb-6">Geser 15 (1 = rendah, 5 = tinggi). Nilai akan dipakai sebagai bobot SAW.</p>
<div class="space-y-6">
@forelse(($criterias ?? collect()) as $c)
@php
$hint = $c->type === 'cost'
? 'Cost semakin kecil semakin baik'
: 'Benefit semakin besar semakin baik';
$val = old('priorities.' . $c->id, 3);
@endphp
<div>
<div class="flex justify-between items-center mb-2">
<label for="priorities_{{ $c->id }}" class="text-sm font-medium text-gray-700">{{ $c->name }}</label>
<span class="inline-flex items-center gap-1.5">
<span class="text-sm font-bold text-primary-600 tabular-nums" id="val-{{ $c->id }}">{{ $val }}</span>
<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-gray-100 text-gray-400 cursor-help text-xs font-bold" title="{{ $hint }}" aria-label="Info">?</span>
</span>
</div>
<input
type="range"
id="priorities_{{ $c->id }}"
name="priorities[{{ $c->id }}]"
value="{{ $val }}"
min="1"
max="5"
step="1"
class="w-full h-3 rounded-full appearance-none bg-gray-200 accent-primary-500 cursor-pointer"
title="{{ $hint }}"
>
<p class="text-xs text-gray-400 mt-0.5">{{ $hint }}</p>
{{-- Step 1: Profil anak --}}
<x-card>
<h3 class="text-xl font-semibold text-gray-900 mb-1">Profil anak</h3>
<p class="text-sm text-gray-500 mb-6">Rentang usia dan budget yang Anda inginkan.</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<x-input label="Usia minimal (tahun)" name="age_min" type="number" value="{{ old('age_min', 3) }}"
min="0" max="18" required />
<x-input label="Usia maksimal (tahun)" name="age_max" type="number" value="{{ old('age_max', 6) }}"
min="0" max="18" required />
<x-input label="Budget minimal (Rp)" name="budget_min" type="number"
value="{{ old('budget_min', 50000) }}" min="0" required />
<x-input label="Budget maksimal (Rp)" name="budget_max" type="number"
value="{{ old('budget_max', 300000) }}" min="0" required />
<div class="col-span-1 sm:col-span-2">
<x-select label="Kategori (opsional)" name="category" id="category">
<option value="">Pilih Kategori</option>
@foreach($categories as $category)
<option value="{{ $category }}" {{ old('category') === $category ? 'selected' : '' }}>
{{ $category }}
</option>
@endforeach
</x-select>
</div>
@empty
<x-alert variant="warning">
Kriteria belum tersedia. Tambahkan kriteria di panel admin terlebih dahulu.
</x-alert>
@endforelse
</div>
</x-card>
</div>
</x-card>
<x-button type="submit" variant="primary" size="lg" class="w-full sm:w-auto">
<i class="fa-solid fa-lightbulb"></i> Dapatkan Rekomendasi
</x-button>
</form>
</div>
@endsection
{{-- Step 2: Prioritas kriteria --}}
<x-card>
<h3 class="text-xl font-semibold text-gray-900 mb-1">Prioritas kriteria</h3>
<p class="text-sm text-gray-500 mb-6">Geser 15. Nilai akan dipakai sebagai bobot
SAW.</p>
<div class="space-y-6">
@forelse(($criterias ?? collect()) as $c)
@php
$hint = $c->type === 'cost'
? 'Cost semakin kecil semakin baik'
: 'Benefit semakin besar semakin baik';
$val = old('priorities.' . $c->id, 3);
@endphp
<div>
<div class="flex justify-between items-center mb-2">
<label for="priorities_{{ $c->id }}"
class="text-sm font-medium text-gray-700">{{ $c->name }}</label>
<span class="inline-flex items-center gap-1.5">
<span class="text-sm font-bold text-primary-600 tabular-nums"
id="val-{{ $c->id }}">{{ $val }}</span>
<span
class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-gray-100 text-gray-400 cursor-help text-xs font-bold"
title="{{ $hint }}" aria-label="Info">?</span>
</span>
</div>
<input type="range" id="priorities_{{ $c->id }}" name="priorities[{{ $c->id }}]" value="{{ $val }}"
min="1" max="5" step="1"
class="w-full h-3 rounded-full appearance-none bg-gray-200 accent-primary-500 cursor-pointer"
title="{{ $hint }}">
<p class="text-xs text-gray-400 mt-0.5">{{ $hint }}</p>
</div>
@empty
<x-alert variant="warning">
Kriteria belum tersedia. Tambahkan kriteria di panel admin terlebih dahulu.
</x-alert>
@endforelse
</div>
</x-card>
<x-button type="submit" variant="primary" size="lg" class="w-full sm:w-auto">
<i class="fa-solid fa-lightbulb"></i> Dapatkan Rekomendasi
</x-button>
</form>
</div>
@endsection