feat: use only meilisearch for hunts

This commit is contained in:
Leon 2026-01-30 10:01:19 +01:00
parent dca9d0bc3d
commit 6887214402
8 changed files with 238 additions and 66 deletions

View File

@ -23,7 +23,8 @@ use Filament\Schemas\Schema;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Dotswan\MapPicker\Fields\Map;
use Filament\Resources\Forms\Form;
class HuntsResource extends Resource class HuntsResource extends Resource
{ {
public static function getEloquentQuery(): Builder public static function getEloquentQuery(): Builder
@ -65,12 +66,50 @@ class HuntsResource extends Resource
TextInput::make('city') TextInput::make('city')
->required() ->required()
->maxLength(255), ->maxLength(255),
Forms\Components\Hidden::make('latitude'),
Forms\Components\Hidden::make('longitude'),
DateTimePicker::make('start_at'), DateTimePicker::make('start_at'),
DateTimePicker::make('end_at'), DateTimePicker::make('end_at'),
TextInput::make('latitude')
->numeric(), // Map
TextInput::make('longitude') Map::make('location')
->numeric(), ->label('Location')
->columnSpanFull()
// Basic Configuration
->defaultLocation(latitude: 40.4168, longitude: -3.7038)
->draggable(true)
->clickable(true) // click to move marker
->zoom(15)
->minZoom(0)
->maxZoom(28)
->tilesUrl("https://tile.openstreetmap.org/{z}/{x}/{y}.png")
->detectRetina(true)
->showFullscreenControl(false)
->showZoomControl(true)
// Location Features
->showMyLocationButton(true)
->boundaries(true, 49.5, -11, 61, 2)
->rangeSelectField('distance')
// Extra Customization
->extraStyles([
'border-radius: 10px'
])
// State Management
->afterStateUpdated(function ($set, ?array $state): void {
$set('latitude', $state['lat']);
$set('longitude', $state['lng']);
})
->afterStateHydrated(function ($state, $record, $set): void {
if ($record && $record->latitude && $record->longitude) {
$set('location', [
'lat' => $record->latitude,
'lng' => $record->longitude,
]);
}
})
]); ]);
} }

View File

@ -22,24 +22,17 @@ class HuntsController extends Controller
try { try {
$searchTerm = $this->resolveSearchTerm($request); $searchTerm = $this->resolveSearchTerm($request);
if ($searchTerm !== null) { // Construire les filtres Meilisearch
$hunts = Hunts::search($searchTerm) $filters = $this->buildMeilisearchFilters($request);
->query(fn (Builder $query) => $this->applyHuntFilters(
$query->with('participants:id,avatar_uri'), // Toujours utiliser Meilisearch (avec ou sans terme de recherche)
$request $hunts = Hunts::search($searchTerm ?? '')
)) ->options([
'filter' => $filters,
])
->query(fn ($query) => $query->with('participants:id,avatar_uri'))
->paginate(10); ->paginate(10);
return response()->json($hunts);
}
$query = $this->applyHuntFilters(
Hunts::with('participants:id,avatar_uri'),
$request
);
$hunts = $query->paginate(10);
return response()->json($hunts); return response()->json($hunts);
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error('Error fetching hunts: '.$e->getMessage()); Log::error('Error fetching hunts: '.$e->getMessage());
@ -51,8 +44,14 @@ class HuntsController extends Controller
} }
} }
private function applyHuntFilters(Builder $query, Request $request): Builder /**
* Build Meilisearch filter string from request parameters
*/
private function buildMeilisearchFilters(Request $request): string
{ {
$filters = [];
// Filtre "participating" (hunts auxquels l'utilisateur participe ou non)
if ($request->has('participating')) { if ($request->has('participating')) {
$participating = filter_var( $participating = filter_var(
$request->query('participating'), $request->query('participating'),
@ -62,58 +61,44 @@ class HuntsController extends Controller
$userId = auth('sanctum')->id(); $userId = auth('sanctum')->id();
Log::info('Participating filter resolved', [ if ($userId) {
'participating' => $participating,
'user_id' => $userId,
]);
if (! $userId) {
if ($participating === true) { if ($participating === true) {
$query->whereRaw('1 = 0'); $filters[] = "participant_ids = {$userId}";
} } elseif ($participating === false) {
} else { $filters[] = "participant_ids != {$userId}";
if ($participating === true) {
$query->whereHas('participants', function ($q) use ($userId) {
$q->where('user_id', $userId);
});
} else {
$query->whereDoesntHave('participants', function ($q) use ($userId) {
$q->where('user_id', $userId);
});
} }
} elseif ($participating === true) {
// User non authentifié mais demande ses participations -> retourne rien
$filters[] = "id < 0"; // Impossible, retourne 0 résultat
} }
} }
// Filtre "active" (hunts actuellement actifs ou non)
if ($request->has('active')) { if ($request->has('active')) {
$now = Carbon::now(); $now = Carbon::now()->timestamp;
$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) {
$query->where('start_at', '<=', $now) // Hunt actif : start_at <= now AND (end_at IS NULL OR end_at >= now)
->where(function ($q) use ($now) { $filters[] = "start_at_timestamp <= {$now}";
$q->whereNull('end_at') $filters[] = "(end_at_timestamp >= {$now} OR end_at_timestamp IS NULL)";
->orWhere('end_at', '>=', $now);
});
} else { } else {
$query->where(function ($q) use ($now) { // Hunt inactif : start_at > now OU end_at < now
$q->whereNull('start_at') $filters[] = "(start_at_timestamp > {$now} OR end_at_timestamp < {$now})";
->orWhere('start_at', '>', $now)
->orWhere(function ($q2) use ($now) {
$q2->whereNotNull('end_at')
->where('end_at', '<', $now);
});
});
} }
} }
// Filtre "city" (recherche par ville)
if ($request->filled('city')) { if ($request->filled('city')) {
$city = $request->query('city'); $city = $request->query('city');
if (is_string($city)) { if (is_string($city)) {
$query->where('city', 'like', '%'.$city.'%'); $city = addslashes($city); // Échapper les guillemets
$filters[] = "city = \"{$city}\"";
} }
} }
// Filtre "difficulty" (niveau de difficulté)
if ($request->filled('difficulty')) { if ($request->filled('difficulty')) {
$difficulties = $request->query('difficulty'); $difficulties = $request->query('difficulty');
@ -128,35 +113,39 @@ class HuntsController extends Controller
)); ));
if (! empty($validDifficulties)) { if (! empty($validDifficulties)) {
$query->whereIn('difficulty', $validDifficulties); $difficultyFilters = array_map(fn($d) => "difficulty = \"{$d}\"", $validDifficulties);
$filters[] = '('.implode(' OR ', $difficultyFilters).')';
} }
} }
} }
// Filtres de dates
$startFrom = $this->parseDateFilter($request->query('start_from')); $startFrom = $this->parseDateFilter($request->query('start_from'));
$startTo = $this->parseDateFilter($request->query('start_to')); $startTo = $this->parseDateFilter($request->query('start_to'));
$endFrom = $this->parseDateFilter($request->query('end_from')); $endFrom = $this->parseDateFilter($request->query('end_from'));
$endTo = $this->parseDateFilter($request->query('end_to')); $endTo = $this->parseDateFilter($request->query('end_to'));
if ($startFrom) { if ($startFrom) {
$query->where('start_at', '>=', $startFrom); $filters[] = "start_at_timestamp >= {$startFrom->timestamp}";
} }
if ($startTo) { if ($startTo) {
$query->where('start_at', '<=', $startTo); $filters[] = "start_at_timestamp <= {$startTo->timestamp}";
} }
if ($endFrom) { if ($endFrom) {
$query->where('end_at', '>=', $endFrom); $filters[] = "end_at_timestamp >= {$endFrom->timestamp}";
} }
if ($endTo) { if ($endTo) {
$query->where('end_at', '<=', $endTo); $filters[] = "end_at_timestamp <= {$endTo->timestamp}";
} }
return $query; // Joindre tous les filtres avec AND
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');

View File

@ -58,6 +58,31 @@ class Hunts extends Model
->withTimestamps(); ->withTimestamps();
} }
/**
* Configure Meilisearch index settings
*/
public function searchableOptions(): array
{
return [
'filterableAttributes' => [
'city',
'difficulty',
'status',
'is_public',
'creator_id',
'participant_ids',
'start_at_timestamp',
'end_at_timestamp',
'created_at_timestamp',
],
'sortableAttributes' => [
'start_at_timestamp',
'end_at_timestamp',
'created_at_timestamp',
],
];
}
/** /**
* @return array{ * @return array{
* id: int, * id: int,
@ -68,8 +93,15 @@ class Hunts extends Model
* status: string, * status: string,
* difficulty: string, * difficulty: string,
* is_public: bool, * is_public: bool,
* creator_id: int,
* latitude: float|null,
* longitude: float|null,
* start_at: string|null, * start_at: string|null,
* end_at: string|null * end_at: string|null,
* start_at_timestamp: int|null,
* end_at_timestamp: int|null,
* created_at_timestamp: int,
* participant_ids: array<int>
* } * }
*/ */
public function toSearchableArray(): array public function toSearchableArray(): array
@ -86,8 +118,17 @@ class Hunts extends Model
'status' => $status, 'status' => $status,
'difficulty' => $difficulty, 'difficulty' => $difficulty,
'is_public' => $this->is_public, 'is_public' => $this->is_public,
'creator_id' => $this->creator_id,
'latitude' => $this->latitude,
'longitude' => $this->longitude,
'start_at' => $this->start_at?->toAtomString(), 'start_at' => $this->start_at?->toAtomString(),
'end_at' => $this->end_at?->toAtomString(), 'end_at' => $this->end_at?->toAtomString(),
// Timestamps Unix pour les filtres de date dans Meilisearch
'start_at_timestamp' => $this->start_at?->timestamp,
'end_at_timestamp' => $this->end_at?->timestamp,
'created_at_timestamp' => $this->created_at->timestamp,
// IDs des participants pour le filtre de participation
'participant_ids' => $this->participants()->pluck('user_id')->toArray(),
]; ];
} }
} }

View File

@ -11,6 +11,7 @@
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"dedoc/scramble": "^0.13.5", "dedoc/scramble": "^0.13.5",
"dotswan/filament-map-picker": "*",
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",
"filament/filament": "^4.0", "filament/filament": "^4.0",
"http-interop/http-factory-guzzle": "*", "http-interop/http-factory-guzzle": "*",

78
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "1b380d582f2a52b93929fa0d443b0384", "content-hash": "8632b5fc1053d477088663c92da4db8c",
"packages": [ "packages": [
{ {
"name": "anourvalar/eloquent-serialize", "name": "anourvalar/eloquent-serialize",
@ -937,6 +937,82 @@
], ],
"time": "2024-02-05T11:56:58+00:00" "time": "2024-02-05T11:56:58+00:00"
}, },
{
"name": "dotswan/filament-map-picker",
"version": "v2.1.3",
"source": {
"type": "git",
"url": "https://github.com/dotswan/filament-map-picker.git",
"reference": "15bb9a4e32b66bfb8a34bbe98b9df4ec9d01da8b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dotswan/filament-map-picker/zipball/15bb9a4e32b66bfb8a34bbe98b9df4ec9d01da8b",
"reference": "15bb9a4e32b66bfb8a34bbe98b9df4ec9d01da8b",
"shasum": ""
},
"require": {
"filament/filament": "^4.0",
"illuminate/contracts": "^10.0 || ^11.0 || ^12.0",
"php": "^8.2",
"spatie/laravel-package-tools": "^1.19.0"
},
"require-dev": {
"laravel/pint": "^1.0",
"nunomaduro/collision": "^7",
"orchestra/testbench": "^8.0|^9.0|^10.0",
"pestphp/pest": "^2.1|^3.1",
"pestphp/pest-plugin-arch": "^2.0",
"pestphp/pest-plugin-laravel": "^2.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"MapPicker": "Dotswan\\MapPicker\\Facades\\MapPicker"
},
"providers": [
"Dotswan\\MapPicker\\MapPickerServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Dotswan\\MapPicker\\": "src/",
"Dotswan\\MapPicker\\Database\\Factories\\": "database/factories/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dotswan",
"email": "tech@dotswan.com",
"role": "Developer"
}
],
"description": "Easily pick and retrieve geo-coordinates using a map-based interface in your Filament applications.",
"homepage": "https://github.com/dotswan/filament-map-picker",
"keywords": [
"dotswan",
"filament",
"filament-map-picker",
"filament-v4",
"filamentphp",
"laravel",
"map-picker"
],
"support": {
"issues": "https://github.com/dotswan/filament-map-picker/issues",
"source": "https://github.com/dotswan/filament-map-picker"
},
"time": "2025-12-25T07:10:54+00:00"
},
{ {
"name": "dragonmantank/cron-expression", "name": "dragonmantank/cron-expression",
"version": "v3.6.0", "version": "v3.6.0",

View File

@ -140,9 +140,24 @@ return [
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'), 'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
'key' => env('MEILISEARCH_KEY'), 'key' => env('MEILISEARCH_KEY'),
'index-settings' => [ 'index-settings' => [
// 'users' => [ 'hunts' => [
// 'filterableAttributes'=> ['id', 'name', 'email'], 'filterableAttributes' => [
// ], 'city',
'difficulty',
'status',
'is_public',
'creator_id',
'participant_ids',
'start_at_timestamp',
'end_at_timestamp',
'created_at_timestamp',
],
'sortableAttributes' => [
'start_at_timestamp',
'end_at_timestamp',
'created_at_timestamp',
],
],
], ],
], ],

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long