feat: try fix hunts meilisearch
This commit is contained in:
parent
3ce5257402
commit
c241e67c90
|
|
@ -3,10 +3,7 @@
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Enums\HuntDifficulty;
|
use App\Enums\HuntDifficulty;
|
||||||
use App\Http\Requests\StoreHuntRequest;
|
|
||||||
use App\Http\Requests\UpdateHuntRequest;
|
|
||||||
use App\Models\Hunts;
|
use App\Models\Hunts;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
@ -14,24 +11,34 @@ use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class HuntsController extends Controller
|
class HuntsController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Display a listing of the resource.
|
|
||||||
*/
|
|
||||||
public function index(Request $request): JsonResponse
|
public function index(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$searchTerm = $this->resolveSearchTerm($request);
|
$searchTerm = $this->resolveSearchTerm($request);
|
||||||
|
|
||||||
// Construire les filtres Meilisearch
|
// Construire les filtres Meilisearch
|
||||||
$filters = $this->buildMeilisearchFilters($request);
|
$filters = $this->buildMeilisearchFilters($request);
|
||||||
|
|
||||||
// Toujours utiliser Meilisearch (avec ou sans terme de recherche)
|
// Construire la recherche
|
||||||
$hunts = Hunts::search($searchTerm ?? '')
|
$search = Hunts::search($searchTerm ?? '')
|
||||||
->options([
|
->query(fn ($query) => $query->with('participants:id,avatar_uri'));
|
||||||
|
|
||||||
|
// N'ajouter le filter que s'il n'est pas vide
|
||||||
|
if ($filters !== '') {
|
||||||
|
$search->options([
|
||||||
'filter' => $filters,
|
'filter' => $filters,
|
||||||
])
|
]);
|
||||||
->query(fn ($query) => $query->with('participants:id,avatar_uri'))
|
}
|
||||||
->paginate(10);
|
|
||||||
|
// (utile en prod pour vérifier ce qui est réellement envoyé)
|
||||||
|
Log::info('HUNTS_SEARCH', [
|
||||||
|
'q' => $searchTerm ?? '',
|
||||||
|
'filters' => $filters,
|
||||||
|
'meili_host' => config('scout.meilisearch.host'),
|
||||||
|
'index' => (new Hunts)->searchableAs(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$hunts = $search->paginate(10);
|
||||||
|
|
||||||
return response()->json($hunts);
|
return response()->json($hunts);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
@ -44,14 +51,11 @@ class HuntsController extends Controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build Meilisearch filter string from request parameters
|
|
||||||
*/
|
|
||||||
private function buildMeilisearchFilters(Request $request): string
|
private function buildMeilisearchFilters(Request $request): string
|
||||||
{
|
{
|
||||||
$filters = [];
|
$filters = [];
|
||||||
|
|
||||||
// Filtre "participating" (hunts auxquels l'utilisateur participe ou non)
|
// participating
|
||||||
if ($request->has('participating')) {
|
if ($request->has('participating')) {
|
||||||
$participating = filter_var(
|
$participating = filter_var(
|
||||||
$request->query('participating'),
|
$request->query('participating'),
|
||||||
|
|
@ -68,14 +72,13 @@ class HuntsController extends Controller
|
||||||
$filters[] = "participant_ids != {$userId}";
|
$filters[] = "participant_ids != {$userId}";
|
||||||
}
|
}
|
||||||
} elseif ($participating === true) {
|
} elseif ($participating === true) {
|
||||||
// User non authentifié mais demande ses participations -> retourne rien
|
$filters[] = "id < 0";
|
||||||
$filters[] = "id < 0"; // Impossible, retourne 0 résultat
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtre "active" (hunts actuellement actifs ou non)
|
// active
|
||||||
if ($request->has('active')) {
|
if ($request->has('active')) {
|
||||||
$now = Carbon::now()->timestamp;
|
$now = Carbon::now('UTC')->timestamp; // ✅ UTC
|
||||||
$active = filter_var($request->query('active'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
$active = filter_var($request->query('active'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||||
|
|
||||||
if ($active === true) {
|
if ($active === true) {
|
||||||
|
|
@ -86,17 +89,17 @@ class HuntsController extends Controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtre "city" (recherche par ville)
|
// city
|
||||||
if ($request->filled('city')) {
|
if ($request->filled('city')) {
|
||||||
$city = $request->query('city');
|
$city = $request->query('city');
|
||||||
|
|
||||||
if (is_string($city)) {
|
if (is_string($city)) {
|
||||||
$city = addslashes($city); // Échapper les guillemets
|
// ✅ échapper correctement les quotes
|
||||||
|
$city = str_replace('"', '\"', $city);
|
||||||
$filters[] = "city = \"{$city}\"";
|
$filters[] = "city = \"{$city}\"";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtre "difficulty" (niveau de difficulté)
|
// difficulty
|
||||||
if ($request->filled('difficulty')) {
|
if ($request->filled('difficulty')) {
|
||||||
$difficulties = $request->query('difficulty');
|
$difficulties = $request->query('difficulty');
|
||||||
|
|
||||||
|
|
@ -110,45 +113,40 @@ class HuntsController extends Controller
|
||||||
array_map('strtolower', $difficulties)
|
array_map('strtolower', $difficulties)
|
||||||
));
|
));
|
||||||
|
|
||||||
if (! empty($validDifficulties)) {
|
if (!empty($validDifficulties)) {
|
||||||
$difficultyFilters = array_map(fn($d) => "difficulty = \"{$d}\"", $validDifficulties);
|
$difficultyFilters = array_map(fn ($d) => "difficulty = \"{$d}\"", $validDifficulties);
|
||||||
$filters[] = '('.implode(' OR ', $difficultyFilters).')';
|
$filters[] = '(' . implode(' OR ', $difficultyFilters) . ')';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtres de dates
|
// ✅ Dates (UTC + end-of-day si nécessaire)
|
||||||
$startFrom = $this->parseDateFilter($request->query('start_from'));
|
$startFrom = $this->parseDateFilterUtc($request->query('start_from'));
|
||||||
$startTo = $this->parseDateFilter($request->query('start_to'));
|
$startTo = $this->parseDateFilterUtc($request->query('start_to'), true);
|
||||||
$endFrom = $this->parseDateFilter($request->query('end_from'));
|
$endFrom = $this->parseDateFilterUtc($request->query('end_from'));
|
||||||
$endTo = $this->parseDateFilter($request->query('end_to'));
|
$endTo = $this->parseDateFilterUtc($request->query('end_to'), true);
|
||||||
|
|
||||||
if ($startFrom) {
|
if ($startFrom) {
|
||||||
$filters[] = "start_at_timestamp >= {$startFrom->timestamp}";
|
$filters[] = "start_at_timestamp >= {$startFrom->timestamp}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($startTo) {
|
if ($startTo) {
|
||||||
$filters[] = "start_at_timestamp <= {$startTo->timestamp}";
|
$filters[] = "start_at_timestamp <= {$startTo->timestamp}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($endFrom) {
|
if ($endFrom) {
|
||||||
$filters[] = "end_at_timestamp >= {$endFrom->timestamp}";
|
$filters[] = "end_at_timestamp >= {$endFrom->timestamp}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($endTo) {
|
if ($endTo) {
|
||||||
$filters[] = "end_at_timestamp <= {$endTo->timestamp}";
|
$filters[] = "end_at_timestamp <= {$endTo->timestamp}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Joindre tous les filtres avec AND
|
|
||||||
return implode(' AND ', $filters);
|
return implode(' AND ', $filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private function resolveSearchTerm(Request $request): ?string
|
private function resolveSearchTerm(Request $request): ?string
|
||||||
{
|
{
|
||||||
$searchTerm = $request->query('search') ?? $request->query('q');
|
$searchTerm = $request->query('search') ?? $request->query('q');
|
||||||
|
|
||||||
if (! is_string($searchTerm)) {
|
if (!is_string($searchTerm)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,149 +155,29 @@ class HuntsController extends Controller
|
||||||
return $searchTerm !== '' ? $searchTerm : null;
|
return $searchTerm !== '' ? $searchTerm : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function parseDateFilter(mixed $value): ?Carbon
|
/**
|
||||||
|
* Parse une date en UTC.
|
||||||
|
* Si $endOfDay=true et que l'entrée ressemble à une date "YYYY-MM-DD",
|
||||||
|
* on la transforme en fin de journée UTC.
|
||||||
|
*/
|
||||||
|
private function parseDateFilterUtc(mixed $value, bool $endOfDay = false): ?Carbon
|
||||||
{
|
{
|
||||||
if (! is_string($value) || trim($value) === '') {
|
if (!is_string($value) || trim($value) === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Carbon::parse($value);
|
$value = trim($value);
|
||||||
|
|
||||||
|
// Si le front envoie une date simple "2026-01-30"
|
||||||
|
if ($endOfDay && preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
||||||
|
return Carbon::createFromFormat('Y-m-d', $value, 'UTC')->endOfDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISO 8601 type "2026-01-29T23:00:00.000Z" => UTC garanti
|
||||||
|
return Carbon::parse($value)->utc();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a newly created resource in storage.
|
|
||||||
*/
|
|
||||||
public function store(StoreHuntRequest $request): JsonResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validated();
|
|
||||||
|
|
||||||
$validated['creator_id'] = auth()->id();
|
|
||||||
|
|
||||||
$hunt = Hunts::create($validated);
|
|
||||||
|
|
||||||
return response()->json($hunt, 201);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the specified resource.
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Display the specified resource.
|
|
||||||
*/
|
|
||||||
public function show(Request $request, int $id): JsonResponse
|
|
||||||
{
|
|
||||||
$hunt = Hunts::with(['steps', 'participants:id,avatar_uri'])->find($id);
|
|
||||||
|
|
||||||
if (! $hunt) {
|
|
||||||
return response()->json(['message' => 'Hunt not found'], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user('sanctum');
|
|
||||||
|
|
||||||
if ($user) {
|
|
||||||
$participation = $user->participations()->where('hunt_id', $hunt->id)->first();
|
|
||||||
|
|
||||||
if ($participation) {
|
|
||||||
// User is a participant
|
|
||||||
$currentStepNumber = $participation->pivot->current_step_number;
|
|
||||||
|
|
||||||
// Filter steps
|
|
||||||
$hunt->setRelation('steps', $hunt->steps->filter(function ($step) use ($currentStepNumber) {
|
|
||||||
if ($step->step_number < $currentStepNumber) {
|
|
||||||
return true; // Show past steps fully
|
|
||||||
}
|
|
||||||
if ($step->step_number == $currentStepNumber) {
|
|
||||||
// Show hints but hide location for current step
|
|
||||||
$step->makeHidden(['latitude', 'longitude', 'radius_m']);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false; // Hide future steps
|
|
||||||
})->values());
|
|
||||||
|
|
||||||
$hunt->participation_status = $participation->pivot;
|
|
||||||
} elseif ($hunt->creator_id !== $user->id) {
|
|
||||||
// User is logged in but not participant and not creator
|
|
||||||
// Show only first step or nothing specific (keep as is for now, maybe hide all steps or just show basic info)
|
|
||||||
// For now, let's just hide hints of all steps if not participating?
|
|
||||||
// Or maybe just show nothing of steps if not participating?
|
|
||||||
// Requirement: "fetch the hunt in question it retrieves me the hunt steps already passed and the next one" implying context of participation.
|
|
||||||
// If not participating, maybe just return basic hunt info without steps?
|
|
||||||
$hunt->unsetRelation('steps');
|
|
||||||
}
|
|
||||||
// If creator, show everything (default)
|
|
||||||
} else {
|
|
||||||
// Guest
|
|
||||||
$hunt->unsetRelation('steps');
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json($hunt);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function participate(Request $request, int $id): JsonResponse
|
|
||||||
{
|
|
||||||
$hunt = Hunts::findOrFail($id);
|
|
||||||
$user = $request->user();
|
|
||||||
|
|
||||||
if ($user->participations()->where('hunt_id', $hunt->id)->exists()) {
|
|
||||||
return response()->json(['message' => 'Already participating'], 409);
|
|
||||||
}
|
|
||||||
|
|
||||||
$hunt->participants()->attach($user->id, ['current_step_number' => 1, 'status' => 'started']);
|
|
||||||
|
|
||||||
return response()->json(['message' => 'Participation started'], 201);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function unparticipate(Request $request, int $id): JsonResponse
|
|
||||||
{
|
|
||||||
$hunt = Hunts::findOrFail($id);
|
|
||||||
$user = $request->user();
|
|
||||||
|
|
||||||
if (! $user->participations()->where('hunt_id', $hunt->id)->exists()) {
|
|
||||||
return response()->json(['message' => 'Not participating'], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$hunt->participants()->detach($user->id);
|
|
||||||
|
|
||||||
return response()->json(['message' => 'Participation stopped'], 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified resource in storage.
|
|
||||||
*/
|
|
||||||
public function update(UpdateHuntRequest $request, int $id): JsonResponse
|
|
||||||
{
|
|
||||||
$hunt = Hunts::find($id);
|
|
||||||
|
|
||||||
if (! $hunt) {
|
|
||||||
return response()->json(['message' => 'Hunt not found'], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$validated = $request->validated();
|
|
||||||
|
|
||||||
$hunt->update($validated);
|
|
||||||
|
|
||||||
return response()->json($hunt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified resource from storage.
|
|
||||||
*/
|
|
||||||
public function destroy(int $id): JsonResponse
|
|
||||||
{
|
|
||||||
$hunt = Hunts::find($id);
|
|
||||||
|
|
||||||
if (! $hunt) {
|
|
||||||
return response()->json(['message' => 'Hunt not found'], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$hunt->delete();
|
|
||||||
|
|
||||||
return response()->json(['message' => 'Hunt deleted successfully']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
#!/usr/bin/env php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Script de test Meilisearch - Vérification du filtre de difficulté
|
|
||||||
*
|
|
||||||
* Usage: php test-meilisearch-filters.php
|
|
||||||
*/
|
|
||||||
|
|
||||||
require __DIR__.'/vendor/autoload.php';
|
|
||||||
|
|
||||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
|
||||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
|
||||||
|
|
||||||
use App\Models\Hunts;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
echo "🔍 Test des filtres Meilisearch\n\n";
|
|
||||||
|
|
||||||
// Test 1: Vérifier que les hunts existent en DB
|
|
||||||
echo "1️⃣ Vérification des hunts dans la base de données:\n";
|
|
||||||
$allHunts = Hunts::all();
|
|
||||||
echo " Total hunts en DB: " . $allHunts->count() . "\n";
|
|
||||||
|
|
||||||
$huntsByDifficulty = $allHunts->groupBy('difficulty');
|
|
||||||
foreach (['easy', 'medium', 'hard'] as $diff) {
|
|
||||||
$count = isset($huntsByDifficulty[$diff]) ? $huntsByDifficulty[$diff]->count() : 0;
|
|
||||||
echo " - $diff: $count hunts\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n";
|
|
||||||
|
|
||||||
// Test 2: Vérifier ce qui est indexé dans Meilisearch
|
|
||||||
echo "2️⃣ Vérification de l'indexation Meilisearch:\n";
|
|
||||||
try {
|
|
||||||
$search = Hunts::search('')->raw();
|
|
||||||
echo " Total hunts indexés: " . $search['estimatedTotalHits'] . "\n";
|
|
||||||
|
|
||||||
if (isset($search['hits'][0])) {
|
|
||||||
echo " Premier hunt indexé:\n";
|
|
||||||
$firstHunt = $search['hits'][0];
|
|
||||||
echo " - ID: " . ($firstHunt['id'] ?? 'N/A') . "\n";
|
|
||||||
echo " - Title: " . ($firstHunt['title'] ?? 'N/A') . "\n";
|
|
||||||
echo " - Difficulty: " . ($firstHunt['difficulty'] ?? 'N/A') . "\n";
|
|
||||||
echo " - City: " . ($firstHunt['city'] ?? 'N/A') . "\n";
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo " ❌ Erreur: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n";
|
|
||||||
|
|
||||||
// Test 3: Test du filtre de difficulté
|
|
||||||
echo "3️⃣ Test du filtre difficulty=medium:\n";
|
|
||||||
try {
|
|
||||||
$mediumHunts = Hunts::search('')
|
|
||||||
->options([
|
|
||||||
'filter' => 'difficulty = "medium"'
|
|
||||||
])
|
|
||||||
->raw();
|
|
||||||
|
|
||||||
echo " Hunts avec difficulty=medium: " . $mediumHunts['estimatedTotalHits'] . "\n";
|
|
||||||
|
|
||||||
if ($mediumHunts['estimatedTotalHits'] > 0 && isset($mediumHunts['hits'][0])) {
|
|
||||||
echo " Premier résultat:\n";
|
|
||||||
echo " - " . $mediumHunts['hits'][0]['title'] . " (difficulty: " . $mediumHunts['hits'][0]['difficulty'] . ")\n";
|
|
||||||
} else {
|
|
||||||
echo " ⚠️ Aucun hunt trouvé avec difficulty=medium\n";
|
|
||||||
echo " Vérifiez que les hunts ont bien été réindexés avec:\n";
|
|
||||||
echo " php artisan scout:import \"App\\Models\\Hunts\"\n";
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo " ❌ Erreur: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n";
|
|
||||||
|
|
||||||
// Test 4: Vérifier les attributs filtrables
|
|
||||||
echo "4️⃣ Vérification des settings Meilisearch:\n";
|
|
||||||
try {
|
|
||||||
$client = app(\Laravel\Scout\EngineManager::class)->engine();
|
|
||||||
$meilisearchClient = $client->engine;
|
|
||||||
|
|
||||||
$index = $meilisearchClient->index('hunts');
|
|
||||||
$settings = $index->getSettings();
|
|
||||||
|
|
||||||
echo " Attributs filtrables: " . implode(', ', $settings['filterableAttributes'] ?? []) . "\n";
|
|
||||||
echo " Attributs triables: " . implode(', ', $settings['sortableAttributes'] ?? []) . "\n";
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo " ❌ Erreur: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n✅ Tests terminés!\n";
|
|
||||||
Loading…
Reference in New Issue