resolveSearchTerm($request); $filters = $this->buildMeilisearchFilters($request); $search = Hunts::search($searchTerm ?? '') ->query(fn ($query) => $query->with('participants:id,avatar_uri')); // ✅ IMPORTANT: n’envoie PAS filter s’il est vide if ($filters !== '') { $search->options(['filter' => $filters]); } Log::info('HUNTS_SEARCH', [ 'q' => $searchTerm ?? '', 'filters' => $filters, 'meili_host' => config('scout.meilisearch.host'), 'index' => (new Hunts)->searchableAs(), ]); return response()->json($search->paginate(10)); } catch (\Throwable $e) { Log::error('Error fetching hunts: '.$e->getMessage()); return response()->json([ 'error' => 'Server Error', 'message' => $e->getMessage(), ], 500); } } private function buildMeilisearchFilters(Request $request): string { $filters = []; // participating if ($request->has('participating')) { $participating = filter_var( $request->query('participating'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); $userId = auth('sanctum')->id(); // chez toi => UUID string if ($userId) { $escapedUserId = $this->escapeMeiliString((string) $userId); if ($participating === true) { // ✅ UUID => string $filters[] = "participant_ids = \"{$escapedUserId}\""; } elseif ($participating === false) { $filters[] = "participant_ids != \"{$escapedUserId}\""; } } elseif ($participating === true) { $filters[] = "id < 0"; } } // active if ($request->has('active')) { $now = Carbon::now('UTC')->timestamp; $active = filter_var($request->query('active'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($active === true) { // Hunt actif = démarré ET (pas terminé OU pas de date de fin) $filters[] = "start_at_timestamp <= {$now}"; // Meilisearch : la syntaxe est 'field EXISTS' et non 'EXISTS field' // Un hunt est actif si end_at_timestamp n'existe pas OU >= now $filters[] = "(end_at_timestamp >= {$now} OR NOT end_at_timestamp EXISTS)"; } elseif ($active === false) { // Hunt non actif = pas encore commencé OU (déjà terminé ET a une date de fin) // Si pas de end_at_timestamp, le hunt ne peut pas être terminé $filters[] = "(start_at_timestamp > {$now} OR (end_at_timestamp < {$now} AND end_at_timestamp EXISTS))"; } } // city if ($request->filled('city')) { $city = $request->query('city'); if (is_string($city)) { $city = $this->escapeMeiliString($city); $filters[] = "city = \"{$city}\""; } } // difficulty if ($request->filled('difficulty')) { $difficulties = $request->query('difficulty'); if (is_string($difficulties)) { $difficulties = explode(',', $difficulties); } if (is_array($difficulties)) { $validDifficulties = array_values(array_intersect( HuntDifficulty::values(), array_map('strtolower', $difficulties) )); if (!empty($validDifficulties)) { $difficultyFilters = array_map( fn ($d) => 'difficulty = "'.$this->escapeMeiliString($d).'"', $validDifficulties ); $filters[] = '(' . implode(' OR ', $difficultyFilters) . ')'; } } } // dates (UTC) $startFrom = $this->parseDateFilterUtc($request->query('start_from')); $startTo = $this->parseDateFilterUtc($request->query('start_to')); $endFrom = $this->parseDateFilterUtc($request->query('end_from')); $endTo = $this->parseDateFilterUtc($request->query('end_to')); if ($startFrom) { $filters[] = "start_at_timestamp >= {$startFrom->timestamp}"; } if ($startTo) { $filters[] = "start_at_timestamp <= {$startTo->timestamp}"; } if ($endFrom) { $filters[] = "end_at_timestamp >= {$endFrom->timestamp}"; } if ($endTo) { $filters[] = "end_at_timestamp <= {$endTo->timestamp}"; } return implode(' AND ', $filters); } private function resolveSearchTerm(Request $request): ?string { $searchTerm = $request->query('search') ?? $request->query('q'); if (!is_string($searchTerm)) { return null; } $searchTerm = trim($searchTerm); return $searchTerm !== '' ? $searchTerm : null; } private function parseDateFilterUtc(mixed $value): ?Carbon { if (!is_string($value) || trim($value) === '') { return null; } try { return Carbon::parse(trim($value))->utc(); } catch (\Throwable $e) { return null; } } private function escapeMeiliString(string $value): string { // Échappe les backslashes et les guillemets pour une string Meilisearch $value = str_replace('\\', '\\\\', $value); return str_replace('"', '\"', $value); } }