diff --git a/app/Http/Controllers/SuggestionController.php b/app/Http/Controllers/SuggestionController.php index 861a51a..f9dbdd2 100644 --- a/app/Http/Controllers/SuggestionController.php +++ b/app/Http/Controllers/SuggestionController.php @@ -2,10 +2,12 @@ namespace App\Http\Controllers; +use App\Models\Suggestion; use App\Models\Utilisateur; use App\Services\GiteaIssueService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Cache; use Illuminate\Validation\Rule; class SuggestionController extends Controller @@ -46,13 +48,104 @@ class SuggestionController extends Controller ."---\n" .'_Ticket créé automatiquement depuis le formulaire de suggestions du site._'; - if ($gitea->createIssue($title, $body)) { - return redirect()->route('suggestions.create') - ->with('success', 'Merci ! Votre suggestion a bien été transmise à l\'équipe.'); + $numero = $gitea->createIssue($title, $body); + + if ($numero === null) { + return back()->withInput()->withErrors([ + 'gitea' => "L'envoi de votre suggestion a échoué. Merci de réessayer plus tard.", + ]); } - return back()->withInput()->withErrors([ - 'gitea' => "L'envoi de votre suggestion a échoué. Merci de réessayer plus tard.", + // La suggestion est conservée en base, liée au ticket, pour le suivi. + Suggestion::create([ + 'ID_Utilisateur' => $user->ID_Utilisateur, + 'Type' => $data['type'], + 'Titre' => $data['titre'], + 'Description' => $data['description'], + 'GiteaIssueNumber' => $numero, ]); + + return redirect()->route('suggestions.index') + ->with('success', 'Merci ! Votre suggestion a bien été transmise à l\'équipe. Vous pouvez suivre sa prise en compte ici.'); + } + + // Liste des suggestions de l'utilisateur, avec l'état de chaque ticket + public function index(GiteaIssueService $gitea) + { + $suggestions = Suggestion::where('ID_Utilisateur', Auth::user()->ID_Utilisateur) + ->orderByDesc('DateCreation') + ->get(); + + // État des tickets (ouvert/fermé, nb de réponses), mis en cache 60 s + // pour ne pas appeler l'API Gitea à chaque affichage de la page. + $etats = []; + + foreach ($suggestions as $suggestion) { + if ($suggestion->GiteaIssueNumber === null) { + continue; + } + + $etat = Cache::remember( + 'gitea.issue.'.$suggestion->GiteaIssueNumber, + 60, + fn () => $gitea->getIssue($suggestion->GiteaIssueNumber) ?? false, + ); + + $etats[$suggestion->ID_Suggestion] = $etat ?: null; + } + + return view('suggestions.index', compact('suggestions', 'etats')); + } + + // Détail d'une suggestion : fil de discussion du ticket Gitea + public function show(Suggestion $suggestion, GiteaIssueService $gitea) + { + abort_unless($suggestion->ID_Utilisateur === Auth::user()->ID_Utilisateur, 403); + + $issue = null; + $commentaires = []; + + if ($suggestion->GiteaIssueNumber !== null) { + $issue = $gitea->getIssue($suggestion->GiteaIssueNumber); + $commentaires = $gitea->getComments($suggestion->GiteaIssueNumber); + + // Tout est lu : le badge « nouvelle réponse » disparaît de la liste. + $suggestion->CommentairesVus = count($commentaires); + $suggestion->save(); + } + + return view('suggestions.show', compact('suggestion', 'issue', 'commentaires')); + } + + // Répondre sur le ticket : le commentaire est publié sur Gitea + public function repondre(Request $request, Suggestion $suggestion, GiteaIssueService $gitea) + { + abort_unless($suggestion->ID_Utilisateur === Auth::user()->ID_Utilisateur, 403); + abort_if($suggestion->GiteaIssueNumber === null, 404); + + $data = $request->validate([ + 'message' => ['required', 'string', 'min:2', 'max:2000'], + ], [ + 'message.required' => 'Votre message est vide.', + 'message.min' => 'Votre message est trop court.', + 'message.max' => 'Votre message ne peut pas dépasser 2000 caractères.', + ]); + + /** @var Utilisateur $user */ + $user = Auth::user(); + + $corps = '**'.($user->Pseudo ?? 'Utilisateur')."** (via le site) :\n\n".$data['message']; + + if (! $gitea->addComment($suggestion->GiteaIssueNumber, $corps)) { + return back()->withInput()->withErrors([ + 'gitea' => "L'envoi de votre réponse a échoué. Merci de réessayer plus tard.", + ]); + } + + // L'état du ticket a changé : on invalide le cache de la liste. + Cache::forget('gitea.issue.'.$suggestion->GiteaIssueNumber); + + return redirect()->route('suggestions.show', $suggestion) + ->with('success', 'Votre réponse a été publiée sur le ticket.'); } } diff --git a/app/Models/Suggestion.php b/app/Models/Suggestion.php new file mode 100644 index 0000000..b285909 --- /dev/null +++ b/app/Models/Suggestion.php @@ -0,0 +1,45 @@ + 'datetime', + ]; + + public function utilisateur(): BelongsTo + { + return $this->belongsTo(Utilisateur::class, 'ID_Utilisateur', 'ID_Utilisateur'); + } +} diff --git a/app/Services/GiteaIssueService.php b/app/Services/GiteaIssueService.php index 6de6bf8..4391a69 100644 --- a/app/Services/GiteaIssueService.php +++ b/app/Services/GiteaIssueService.php @@ -2,6 +2,7 @@ namespace App\Services; +use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -10,37 +11,132 @@ class GiteaIssueService /** * Crée un ticket (issue) dans le dépôt Gitea configuré. * - * @return bool true si le ticket a bien été créé, false sinon. + * @return int|null le numéro du ticket créé, ou null en cas d'échec. */ - public function createIssue(string $title, string $body): bool + public function createIssue(string $title, string $body): ?int { - $url = rtrim((string) config('services.gitea.url'), '/'); - $repo = trim((string) config('services.gitea.repo'), '/'); - $token = (string) config('services.gitea.token'); + $client = $this->client(); - if ($url === '' || $repo === '' || $token === '') { - Log::warning('Gitea non configuré (URL / token / dépôt manquant) : ticket non créé.'); + if ($client === null) { + return null; + } + try { + $response = $client->post($this->repoPath().'/issues', [ + 'title' => $title, + 'body' => $body, + ]); + } catch (\Throwable $e) { + Log::error('Appel API Gitea impossible : '.$e->getMessage()); + + return null; + } + + if ($response->failed()) { + Log::error('Création du ticket Gitea échouée', [ + 'status' => $response->status(), + 'body' => $response->body(), + ]); + + return null; + } + + return (int) $response->json('number'); + } + + /** + * Récupère l'état d'un ticket (état ouvert/fermé, nombre de commentaires). + * + * @return array{state: string, comments: int, title: string}|null + */ + public function getIssue(int $number): ?array + { + $client = $this->client(); + + if ($client === null) { + return null; + } + + try { + $response = $client->get($this->repoPath().'/issues/'.$number); + } catch (\Throwable $e) { + Log::error('Lecture du ticket Gitea impossible : '.$e->getMessage()); + + return null; + } + + if ($response->failed()) { + return null; + } + + return [ + 'state' => (string) $response->json('state'), + 'comments' => (int) $response->json('comments'), + 'title' => (string) $response->json('title'), + ]; + } + + /** + * Récupère les commentaires (réponses) d'un ticket. + * + * @return list + */ + public function getComments(int $number): array + { + $client = $this->client(); + + if ($client === null) { + return []; + } + + try { + $response = $client->get($this->repoPath().'/issues/'.$number.'/comments'); + } catch (\Throwable $e) { + Log::error('Lecture des commentaires Gitea impossible : '.$e->getMessage()); + + return []; + } + + if ($response->failed()) { + return []; + } + + $commentaires = []; + + foreach ($response->json() ?? [] as $commentaire) { + $commentaires[] = [ + 'auteur' => (string) ($commentaire['user']['username'] ?? $commentaire['user']['login'] ?? 'inconnu'), + 'corps' => (string) ($commentaire['body'] ?? ''), + 'date' => (string) ($commentaire['created_at'] ?? ''), + ]; + } + + return $commentaires; + } + + /** + * Ajoute un commentaire (réponse de l'utilisateur) sur un ticket. + */ + public function addComment(int $number, string $body): bool + { + $client = $this->client(); + + if ($client === null) { return false; } try { - // Gitea attend l'en-tête "Authorization: token ". - $response = Http::withToken($token, 'token') - ->acceptJson() - ->timeout(10) - ->post("{$url}/api/v1/repos/{$repo}/issues", [ - 'title' => $title, - 'body' => $body, - ]); + $response = $client->post($this->repoPath().'/issues/'.$number.'/comments', [ + 'body' => $body, + ]); } catch (\Throwable $e) { - Log::error('Appel API Gitea impossible : '.$e->getMessage()); + Log::error('Ajout du commentaire Gitea impossible : '.$e->getMessage()); return false; } if ($response->failed()) { - Log::error('Création du ticket Gitea échouée', [ + Log::error('Ajout du commentaire Gitea échoué', [ 'status' => $response->status(), 'body' => $response->body(), ]); @@ -50,4 +146,35 @@ class GiteaIssueService return true; } + + /** + * Client HTTP configuré, ou null si la configuration Gitea est incomplète. + */ + private function client(): ?PendingRequest + { + $url = rtrim((string) config('services.gitea.url'), '/'); + $token = (string) config('services.gitea.token'); + + if ($url === '' || $this->repo() === '' || $token === '') { + Log::warning('Gitea non configuré (URL / token / dépôt manquant).'); + + return null; + } + + // Gitea attend l'en-tête "Authorization: token ". + return Http::baseUrl($url) + ->withToken($token, 'token') + ->acceptJson() + ->timeout(10); + } + + private function repo(): string + { + return trim((string) config('services.gitea.repo'), '/'); + } + + private function repoPath(): string + { + return '/api/v1/repos/'.$this->repo(); + } } diff --git a/database/migrations/2026_07_08_100000_create_suggestions_table.php b/database/migrations/2026_07_08_100000_create_suggestions_table.php new file mode 100644 index 0000000..c49e81b --- /dev/null +++ b/database/migrations/2026_07_08_100000_create_suggestions_table.php @@ -0,0 +1,31 @@ +id('ID_Suggestion'); + $table->foreignId('ID_Utilisateur')->constrained('utilisateurs', 'ID_Utilisateur')->onDelete('cascade'); + $table->string('Type', 50); + $table->string('Titre', 150); + $table->text('Description'); + $table->unsignedInteger('GiteaIssueNumber')->nullable(); + // Nombre de commentaires déjà consultés (pour le badge « nouvelle réponse ») + $table->unsignedInteger('CommentairesVus')->default(0); + $table->timestamp('DateCreation')->useCurrent(); + }); + } + + public function down(): void + { + Schema::dropIfExists('suggestions'); + } +}; diff --git a/resources/views/layout/header.blade.php b/resources/views/layout/header.blade.php index f798e2c..27d10fe 100644 --- a/resources/views/layout/header.blade.php +++ b/resources/views/layout/header.blade.php @@ -42,7 +42,10 @@