api/app/Http/Controllers/HuntsController.php

306 lines
9.8 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Enums\HuntDifficulty;
use App\Http\Requests\StoreHuntRequest;
use App\Http\Requests\UpdateHuntRequest;
use App\Models\Hunts;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
class HuntsController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): JsonResponse
{
try {
$searchTerm = $this->resolveSearchTerm($request);
// Construire les filtres Meilisearch
$filters = $this->buildMeilisearchFilters($request);
// Toujours utiliser Meilisearch (avec ou sans terme de recherche)
$hunts = Hunts::search($searchTerm ?? '')
->options([
'filter' => $filters,
])
->query(fn ($query) => $query->with('participants:id,avatar_uri'))
->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);
}
}
/**
* 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')) {
$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) {
// 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')) {
$now = Carbon::now()->timestamp;
$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})";
}
}
// Filtre "city" (recherche par ville)
if ($request->filled('city')) {
$city = $request->query('city');
if (is_string($city)) {
$city = addslashes($city); // Échapper les guillemets
$filters[] = "city = \"{$city}\"";
}
}
// Filtre "difficulty" (niveau de difficulté)
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).')';
}
}
}
// Filtres de dates
$startFrom = $this->parseDateFilter($request->query('start_from'));
$startTo = $this->parseDateFilter($request->query('start_to'));
$endFrom = $this->parseDateFilter($request->query('end_from'));
$endTo = $this->parseDateFilter($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}";
}
// Joindre tous les filtres avec AND
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 parseDateFilter(mixed $value): ?Carbon
{
if (! is_string($value) || trim($value) === '') {
return null;
}
try {
return Carbon::parse($value);
} catch (\Throwable $e) {
return null;
}
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreHuntRequest $request): JsonResponse
{
$validated = $request->validated();
$validated['creator_id'] = auth()->id();
$hunt = Hunts::create($validated);
return response()->json($hunt, 201);
}
/**
* Display the specified resource.
*/
/**
* Display the specified resource.
*/
public function show(Request $request, int $id): JsonResponse
{
$hunt = Hunts::with(['steps', 'participants:id,avatar_uri'])->find($id);
if (! $hunt) {
return response()->json(['message' => 'Hunt not found'], 404);
}
$user = $request->user('sanctum');
if ($user) {
$participation = $user->participations()->where('hunt_id', $hunt->id)->first();
if ($participation) {
// User is a participant
$currentStepNumber = $participation->pivot->current_step_number;
// Filter steps
$hunt->setRelation('steps', $hunt->steps->filter(function ($step) use ($currentStepNumber) {
if ($step->step_number < $currentStepNumber) {
return true; // Show past steps fully
}
if ($step->step_number == $currentStepNumber) {
// Show hints but hide location for current step
$step->makeHidden(['latitude', 'longitude', 'radius_m']);
return true;
}
return false; // Hide future steps
})->values());
$hunt->participation_status = $participation->pivot;
} elseif ($hunt->creator_id !== $user->id) {
// User is logged in but not participant and not creator
// Show only first step or nothing specific (keep as is for now, maybe hide all steps or just show basic info)
// For now, let's just hide hints of all steps if not participating?
// Or maybe just show nothing of steps if not participating?
// Requirement: "fetch the hunt in question it retrieves me the hunt steps already passed and the next one" implying context of participation.
// If not participating, maybe just return basic hunt info without steps?
$hunt->unsetRelation('steps');
}
// If creator, show everything (default)
} else {
// Guest
$hunt->unsetRelation('steps');
}
return response()->json($hunt);
}
public function participate(Request $request, int $id): JsonResponse
{
$hunt = Hunts::findOrFail($id);
$user = $request->user();
if ($user->participations()->where('hunt_id', $hunt->id)->exists()) {
return response()->json(['message' => 'Already participating'], 409);
}
$hunt->participants()->attach($user->id, ['current_step_number' => 1, 'status' => 'started']);
return response()->json(['message' => 'Participation started'], 201);
}
public function unparticipate(Request $request, int $id): JsonResponse
{
$hunt = Hunts::findOrFail($id);
$user = $request->user();
if (! $user->participations()->where('hunt_id', $hunt->id)->exists()) {
return response()->json(['message' => 'Not participating'], 404);
}
$hunt->participants()->detach($user->id);
return response()->json(['message' => 'Participation stopped'], 200);
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateHuntRequest $request, int $id): JsonResponse
{
$hunt = Hunts::find($id);
if (! $hunt) {
return response()->json(['message' => 'Hunt not found'], 404);
}
$validated = $request->validated();
$hunt->update($validated);
return response()->json($hunt);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(int $id): JsonResponse
{
$hunt = Hunts::find($id);
if (! $hunt) {
return response()->json(['message' => 'Hunt not found'], 404);
}
$hunt->delete();
return response()->json(['message' => 'Hunt deleted successfully']);
}
}