180 lines
6.0 KiB
PHP
180 lines
6.0 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Enums\HuntDifficulty;
|
||
use App\Models\Hunts;
|
||
use Illuminate\Http\JsonResponse;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Facades\Log;
|
||
|
||
class HuntsController extends Controller
|
||
{
|
||
public function index(Request $request): JsonResponse
|
||
{
|
||
try {
|
||
$searchTerm = $this->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);
|
||
}
|
||
}
|