Files
Inventory/src/Controller/CommentController.php
Matthieu 476060cf7d WIP
2026-03-31 17:57:59 +02:00

293 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Comment;
use App\Entity\Document;
use App\Enum\DocumentType;
use App\Repository\ProfileRepository;
use App\Service\DocumentStorageService;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/api/comments')]
final class CommentController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly ProfileRepository $profiles,
private readonly DocumentStorageService $storageService,
) {}
#[Route('', name: 'api_comments_create', methods: ['POST'])]
public function create(Request $request): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$session = $request->getSession();
$profileId = $session->get('profileId');
if (!$profileId) {
return $this->json(['message' => 'Aucun profil actif.'], 401);
}
$profile = $this->profiles->find($profileId);
if (!$profile) {
return $this->json(['message' => 'Profil introuvable.'], 401);
}
// Parse fields from JSON or form-data
$contentType = $request->headers->get('Content-Type', '');
$isFormData = str_contains($contentType, 'multipart/form-data') || $request->files->count() > 0 || $request->request->has('content');
if ($isFormData) {
$content = trim((string) $request->request->get('content', ''));
$entityType = trim((string) $request->request->get('entityType', ''));
$entityId = trim((string) $request->request->get('entityId', ''));
$entityName = $request->request->get('entityName') ? trim((string) $request->request->get('entityName')) : null;
} else {
$payload = json_decode($request->getContent(), true);
if (!is_array($payload)) {
return $this->json(['message' => 'Payload JSON invalide.'], 400);
}
$content = trim((string) ($payload['content'] ?? ''));
$entityType = trim((string) ($payload['entityType'] ?? ''));
$entityId = trim((string) ($payload['entityId'] ?? ''));
$entityName = isset($payload['entityName']) ? trim((string) $payload['entityName']) : null;
}
if ('' === $content) {
return $this->json(['message' => 'Le contenu est requis.'], 400);
}
$allowedTypes = ['machine', 'piece', 'composant', 'product', 'piece_category', 'component_category', 'product_category', 'machine_skeleton'];
if (!in_array($entityType, $allowedTypes, true)) {
return $this->json(['message' => 'Type d\'entité invalide.'], 400);
}
if ('' === $entityId) {
return $this->json(['message' => 'L\'identifiant de l\'entité est requis.'], 400);
}
$authorName = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
if ('' === $authorName) {
$authorName = $profile->getEmail() ?? 'Inconnu';
}
$comment = new Comment();
$comment->setContent($content);
$comment->setEntityType($entityType);
$comment->setEntityId($entityId);
$comment->setEntityName($entityName);
$comment->setAuthorId($profileId);
$comment->setAuthorName($authorName);
$this->entityManager->persist($comment);
// Handle file uploads
$allowedMimeTypes = [
'application/pdf',
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp',
'text/plain', 'text/csv',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/msword', 'application/vnd.ms-excel',
'application/zip',
];
$files = $request->files->all('files');
foreach ($files as $file) {
if (!$file instanceof UploadedFile || !$file->isValid()) {
continue;
}
$detectedMime = $file->getMimeType() ?: 'application/octet-stream';
if (!in_array($detectedMime, $allowedMimeTypes, true)) {
return $this->json([
'message' => sprintf('Type de fichier non autorisé : %s', $detectedMime),
], 400);
}
$document = new Document();
$documentId = 'cl'.bin2hex(random_bytes(12));
$document->setId($documentId);
$document->setName($file->getClientOriginalName());
$document->setFilename($file->getClientOriginalName());
$document->setMimeType($file->getMimeType() ?: 'application/octet-stream');
$document->setSize((int) $file->getSize());
$document->setType(DocumentType::DOCUMENTATION);
$document->setComment($comment);
$comment->getDocuments()->add($document);
$extension = $this->storageService->extensionFromFilename($file->getClientOriginalName());
$relativePath = $this->storageService->storeFromPath(
$file->getPathname(),
$documentId,
$extension,
);
$document->setPath($relativePath);
$this->entityManager->persist($document);
}
$this->entityManager->flush();
return $this->json($this->normalize($comment), 201);
}
#[Route('/{id}/resolve', name: 'api_comments_resolve', methods: ['PATCH'])]
public function resolve(string $id, Request $request): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
$comment = $this->entityManager->getRepository(Comment::class)->find($id);
if (!$comment) {
return $this->json(['message' => 'Commentaire introuvable.'], 404);
}
$session = $request->getSession();
$profileId = $session->get('profileId');
$profile = $profileId ? $this->profiles->find($profileId) : null;
$resolverName = 'Inconnu';
if ($profile) {
$resolverName = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
if ('' === $resolverName) {
$resolverName = $profile->getEmail() ?? 'Inconnu';
}
}
$comment->setStatus('resolved');
$comment->setResolvedById($profileId);
$comment->setResolvedByName($resolverName);
$comment->setResolvedAt(new DateTimeImmutable());
$this->entityManager->flush();
return $this->json($this->normalize($comment));
}
#[Route('/search/list', name: 'api_comments_list', methods: ['GET'])]
public function list(Request $request): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$qb = $this->entityManager->getRepository(Comment::class)->createQueryBuilder('c');
$status = $request->query->get('status');
if ($status) {
$qb->andWhere('c.status = :status')->setParameter('status', $status);
}
$entityType = $request->query->get('entityType');
if ($entityType) {
$qb->andWhere('c.entityType = :entityType')->setParameter('entityType', $entityType);
}
$entityName = $request->query->get('entityName');
if ($entityName) {
$qb->andWhere('LOWER(c.entityName) LIKE LOWER(:entityName)')->setParameter('entityName', '%'.$entityName.'%');
}
// Count total before pagination
$countQb = clone $qb;
$total = (int) $countQb->select('COUNT(c.id)')->getQuery()->getSingleScalarResult();
// Sorting
$sortField = $request->query->get('sort', 'createdAt');
$sortDir = strtoupper($request->query->get('direction', 'DESC'));
$allowedSortFields = ['createdAt', 'authorName', 'status'];
if (!in_array($sortField, $allowedSortFields, true)) {
$sortField = 'createdAt';
}
if (!in_array($sortDir, ['ASC', 'DESC'], true)) {
$sortDir = 'DESC';
}
$qb->orderBy('c.'.$sortField, $sortDir);
// Pagination
$itemsPerPage = min((int) $request->query->get('itemsPerPage', '30'), 200);
$page = max((int) $request->query->get('page', '1'), 1);
$qb->setMaxResults($itemsPerPage)->setFirstResult(($page - 1) * $itemsPerPage);
$comments = $qb->getQuery()->getResult();
return $this->json([
'items' => array_map(fn (Comment $c) => $this->normalize($c), $comments),
'total' => $total,
]);
}
#[Route('/by-entity/{entityType}/{entityId}', name: 'api_comments_by_entity', methods: ['GET'])]
public function listByEntity(string $entityType, string $entityId, Request $request): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$criteria = ['entityType' => $entityType, 'entityId' => $entityId];
$status = $request->query->get('status');
if ($status) {
$criteria['status'] = $status;
}
$comments = $this->entityManager->getRepository(Comment::class)
->findBy($criteria, ['createdAt' => 'DESC'])
;
return $this->json(array_map(fn (Comment $c) => $this->normalize($c), $comments));
}
#[Route('/stats/unresolved-count', name: 'api_comments_unresolved_count', methods: ['GET'])]
public function unresolvedCount(): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$count = $this->entityManager->getRepository(Comment::class)
->count(['status' => 'open'])
;
return $this->json(['count' => $count]);
}
private function normalize(Comment $comment): array
{
$documents = [];
foreach ($comment->getDocuments() as $document) {
$documents[] = [
'id' => $document->getId(),
'name' => $document->getName(),
'filename' => $document->getFilename(),
'mimeType' => $document->getMimeType(),
'size' => $document->getSize(),
'type' => $document->getType()->value,
'fileUrl' => '/api/documents/'.$document->getId().'/file',
'downloadUrl' => '/api/documents/'.$document->getId().'/download',
'createdAt' => $document->getCreatedAt()->format(DateTimeInterface::ATOM),
];
}
return [
'id' => $comment->getId(),
'content' => $comment->getContent(),
'entityType' => $comment->getEntityType(),
'entityId' => $comment->getEntityId(),
'entityName' => $comment->getEntityName(),
'authorId' => $comment->getAuthorId(),
'authorName' => $comment->getAuthorName(),
'status' => $comment->getStatus(),
'resolvedById' => $comment->getResolvedById(),
'resolvedByName' => $comment->getResolvedByName(),
'resolvedAt' => $comment->getResolvedAt()?->format(DateTimeInterface::ATOM),
'createdAt' => $comment->getCreatedAt()->format(DateTimeInterface::ATOM),
'updatedAt' => $comment->getUpdatedAt()->format(DateTimeInterface::ATOM),
'documents' => $documents,
];
}
}