api/app/Http/Controllers/HuntsController.php

184 lines
5.9 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);
// 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;
}
}
}