<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\User;
use App\Entity\FeedbackScreenshot;
use App\Repository\FeedbackScreenshotRepository;
use App\Service\FeedbackService;
use App\Service\NotionFeedbackService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Annotation\Route;
/**
* Contrôleur pour la gestion des tickets de feedback.
*
* Expose les API pour la création de tickets et la récupération des screenshots.
* Accessible aux admins, et aux utilisateurs du client id=1 (Bambboo).
*
* @Route("/api/feedback")
*/
class FeedbackController extends AbstractController
{
/**
* @var FeedbackService
*/
private $feedbackService;
/**
* @var NotionFeedbackService
*/
private $notionService;
/**
* @var FeedbackScreenshotRepository
*/
private $screenshotRepository;
/**
* Constructeur du contrôleur.
*
* @param FeedbackService $feedbackService
* @param NotionFeedbackService $notionService
* @param FeedbackScreenshotRepository $screenshotRepository
*/
public function __construct(
FeedbackService $feedbackService,
NotionFeedbackService $notionService,
FeedbackScreenshotRepository $screenshotRepository
) {
$this->feedbackService = $feedbackService;
$this->notionService = $notionService;
$this->screenshotRepository = $screenshotRepository;
}
/**
* Crée un nouveau ticket de feedback.
*
* @Route("", name="feedback.create", methods={"POST"})
*
* @param Request $request
* @return JsonResponse
*/
public function create(Request $request): JsonResponse
{
try {
$user = $this->getUser();
if (!$user instanceof User) {
return $this->json(['error' => 'Utilisateur non authentifié'], Response::HTTP_UNAUTHORIZED);
}
if (!$this->isGranted('ROLE_ADMIN')) {
if (!$user->getClient() || (int) $user->getClient()->getId() !== 1) {
return $this->json(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
}
}
// Récupération des données JSON
$content = $request->getContent();
$data = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return $this->json(['error' => 'Payload JSON invalide'], Response::HTTP_BAD_REQUEST);
}
// Extraction des screenshots du payload
$screenshots = $data['screenshots'] ?? [];
unset($data['screenshots']);
// Création du ticket
$ticket = $this->feedbackService->createTicket($user, $data, $screenshots);
// Synchronisation avec Notion
$notionPageId = null;
$warnings = [];
if ($this->notionService->isConfigured()) {
$notionPageId = $this->notionService->createFeedbackPage($ticket);
if ($notionPageId) {
$this->feedbackService->setNotionPageId($ticket, $notionPageId);
} else {
$warnings[] = [
'source' => 'notion',
'message' => 'Ticket enregistré, mais la synchronisation Notion a échoué.',
];
}
}
// Construction de la réponse
$response = [
'data' => $this->serializeTicket($ticket),
];
if (!empty($warnings)) {
$response['warnings'] = $warnings;
return $this->json($response, Response::HTTP_ACCEPTED);
}
return $this->json($response, Response::HTTP_CREATED);
} catch (\InvalidArgumentException $e) {
$errors = json_decode($e->getMessage(), true);
return $this->json([
'error' => 'Payload invalide',
'details' => $errors,
], Response::HTTP_BAD_REQUEST);
} catch (\Exception $e) {
return $this->json([
'error' => 'Impossible d\'enregistrer le ticket',
'message' => $e->getMessage(),
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Récupère un screenshot par son ID.
* Accessible publiquement pour permettre l'affichage dans Notion.
*
* @Route("/screenshots/{id}", name="feedback.screenshot", methods={"GET"}, requirements={"id"="\d+"})
*
* @param int $id
* @return Response
*/
public function getScreenshot(int $id): Response
{
$screenshot = $this->screenshotRepository->find($id);
if (!$screenshot) {
return $this->json(['error' => 'Screenshot non trouvé'], Response::HTTP_NOT_FOUND);
}
// Récupération du fichier
$filePath = $this->feedbackService->getScreenshotFullPath($screenshot);
if (!file_exists($filePath)) {
return $this->json(['error' => 'Fichier non trouvé'], Response::HTTP_NOT_FOUND);
}
// Envoi du fichier
$response = new BinaryFileResponse($filePath);
$response->headers->set('Content-Type', $screenshot->getMimeType());
$response->headers->set('Content-Length', (string) $screenshot->getSizeBytes());
$response->setContentDisposition(
ResponseHeaderBag::DISPOSITION_INLINE,
$screenshot->getFilename()
);
// Cache d'un an (fichiers immuables)
$response->setPublic();
$response->setMaxAge(31536000);
$response->setImmutable(true);
return $response;
}
/**
* Liste les tickets de feedback (pour les admins).
*
* @Route("", name="feedback.list", methods={"GET"})
* @IsGranted("ROLE_ADMIN")
*
* @param Request $request
* @return JsonResponse
*/
public function list(Request $request): JsonResponse
{
$limit = min((int) $request->query->get('limit', 20), 100);
$offset = (int) $request->query->get('offset', 0);
$ticketRepository = $this->feedbackService->getTicket(0); // Hack pour accéder au repo via le service
// On utilise directement le repository injecté via le service
// Récupération simplifiée via EntityManager
$em = $this->getDoctrine()->getManager();
$tickets = $em->getRepository(\App\Entity\FeedbackTicket::class)
->findBy([], ['createdAt' => 'DESC'], $limit, $offset);
$data = [];
foreach ($tickets as $ticket) {
$data[] = $this->serializeTicket($ticket);
}
return $this->json(['data' => $data]);
}
/**
* Sérialise un ticket pour la réponse JSON.
*
* @param \App\Entity\FeedbackTicket $ticket
* @return array
*/
private function serializeTicket(\App\Entity\FeedbackTicket $ticket): array
{
$createdBy = $ticket->getCreatedBy();
$serialized = [
'id' => $ticket->getId(),
'type' => $ticket->getType(),
'status' => $ticket->getStatus(),
'priorityScore' => $ticket->getPriorityScore(),
'priorityLabel' => $ticket->getPriorityLabel(),
'title' => $ticket->getTitle(),
'issueDescription' => $ticket->getIssueDescription(),
'expectedBehavior' => $ticket->getExpectedBehavior(),
'pageUrl' => $ticket->getPageUrl(),
'notionPageId' => $ticket->getNotionPageId(),
'createdAt' => $ticket->getCreatedAt()->format(\DateTime::ATOM),
'updatedAt' => $ticket->getUpdatedAt()->format(\DateTime::ATOM),
'resolvedAt' => $ticket->getResolvedAt() ? $ticket->getResolvedAt()->format(\DateTime::ATOM) : null,
'reporter' => $createdBy ? [
'id' => $createdBy->getId(),
'email' => $createdBy->getEmail(),
'firstName' => $createdBy->getFirstname(),
'lastName' => $createdBy->getLastname(),
] : null,
'screenshots' => [],
];
// Ajout des screenshots
foreach ($ticket->getScreenshots() as $screenshot) {
$serialized['screenshots'][] = [
'id' => $screenshot->getId(),
'filename' => $screenshot->getFilename(),
'mimeType' => $screenshot->getMimeType(),
'sizeBytes' => $screenshot->getSizeBytes(),
'formattedSize' => $screenshot->getFormattedSize(),
'isCapture' => $screenshot->isCapture(),
'pageUrl' => $screenshot->getPageUrl(),
'createdAt' => $screenshot->getCreatedAt()->format(\DateTime::ATOM),
];
}
return $serialized;
}
}