Filter Kategori Mainan
This commit is contained in:
parent
e68e59a169
commit
6eaf12ef7b
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 1–5 (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 1–5. 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
|
||||
Loading…
Reference in New Issue