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, ]; } }