resolveSearchTerm($request); // Construire les filtres Meilisearch $filters = $this->buildMeilisearchFilters($request); // Construire la recherche $search = Hunts::search($searchTerm ?? '') ->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, ]); } // (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); } 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(); if ($userId) { if ($participating === true) { $filters[] = "participant_ids = {$userId}"; } elseif ($participating === false) { $filters[] = "participant_ids != {$userId}"; } } elseif ($participating === true) { $filters[] = "id < 0"; } } // active if ($request->has('active')) { $now = Carbon::now('UTC')->timestamp; // ✅ UTC $active = filter_var($request->query('active'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($active === true) { $filters[] = "start_at_timestamp <= {$now}"; $filters[] = "(end_at_timestamp >= {$now} OR end_at_timestamp IS NULL)"; } else { $filters[] = "(start_at_timestamp > {$now} OR end_at_timestamp < {$now})"; } } // city if ($request->filled('city')) { $city = $request->query('city'); if (is_string($city)) { // ✅ échapper correctement les quotes $city = str_replace('"', '\"', $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 = \"{$d}\"", $validDifficulties); $filters[] = '(' . implode(' OR ', $difficultyFilters) . ')'; } } } // ✅ Dates (UTC + end-of-day si nécessaire) $startFrom = $this->parseDateFilterUtc($request->query('start_from')); $startTo = $this->parseDateFilterUtc($request->query('start_to'), true); $endFrom = $this->parseDateFilterUtc($request->query('end_from')); $endTo = $this->parseDateFilterUtc($request->query('end_to'), true); 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; } /** * 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) === '') { return null; } try { $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) { return null; } } }