src/Controller/Api/v3/TodoController.php line 203

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Api\v3;
  3. use App\Controller\Api\Service\TodoCommentService;
  4. use App\Controller\Api\Service\TodoService;
  5. use App\Entity\TodoComment;
  6. use App\Entity\TodoNotification;
  7. use App\Entity\Todos;
  8. use App\Entity\User;
  9. use App\Enum\SeverityInterface;
  10. use App\Service\SerializeService\TodoSerialize;
  11. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  12. use Symfony\Component\HttpFoundation\Request;
  13. use Symfony\Component\HttpFoundation\Response;
  14. use Symfony\Component\Routing\Annotation\Route;
  15. /**
  16.  * Todo Board API.
  17.  *
  18.  * Akis: Controller (request) -> Service (tum process: find + erisim + DB) -> entity
  19.  *       -> Controller (DI ile alinan Serialize servisi) -> SerializedResponse -> FE
  20.  *
  21.  * Payload Service'e `setParams($request->request)` / `setFiles($request->files)` ile
  22.  * gecer (AbstractControllerService protokolu). Oturumdaki kullanici ve roller
  23.  * Service icinde `$this->security`'den okunur — Controller'dan tasinmaz.
  24.  *
  25.  * @Route("/api/v3/todo")
  26.  */
  27. class TodoController extends AbstractController
  28. {
  29.     private TodoService $todoService;
  30.     private TodoSerialize $todoSerialize;
  31.     private TodoCommentService $commentService;
  32.     public function __construct(
  33.         TodoService $todoService,
  34.         TodoSerialize $todoSerialize,
  35.         TodoCommentService $commentService
  36.     ) {
  37.         $this->todoService    = $todoService;
  38.         $this->todoSerialize  = $todoSerialize;
  39.         $this->commentService = $commentService;
  40.     }
  41.     /**
  42.      * Tek todo donen endpoint'lerin ortak yaniti — DI ile alinan Serialize
  43.      * servisi burada (Controller'da) calisir; Service entity dondurur.
  44.      */
  45.     private function todoResponse(Todos $todo): Response
  46.     {
  47.         $countMap = $this->commentService->countByTodoIds([$todo->getId()]);
  48.         $serialized                  = $this->todoSerialize->setEntity($todo)->setGroups($this->todoSerialize->_groupCore())->render();
  49.         $serialized['comment_count'] = $countMap[$todo->getId()] ?? 0;
  50.         return $this->json([
  51.             'todo'               => $serialized,
  52.             'notified_admin_ids' => array_values($this->todoService->getLastNotifiedAdminIds()),
  53.             'severity'           => SeverityInterface::SUCCESS,
  54.         ]);
  55.     }
  56.     /**
  57.      * @Route("/todos", name="api_v3_todo_todos", methods={"GET"})
  58.      */
  59.     public function todos(): Response
  60.     {
  61.         $todos    = $this->todoService->fetchTodos();
  62.         $ids      = array_map(function (Todos $t) { return $t->getId(); }, $todos);
  63.         $countMap = $this->commentService->countByTodoIds($ids);
  64.         $serialized = $this->todoSerialize->setEntity($todos)->setGroups($this->todoSerialize->_groupCore())->render();
  65.         foreach ($serialized as &$item) {
  66.             $item['comment_count'] = $countMap[$item['id']] ?? 0;
  67.         }
  68.         unset($item);
  69.         return $this->json([
  70.             'todos'    => array_values($serialized),
  71.             'severity' => SeverityInterface::SUCCESS,
  72.         ]);
  73.     }
  74.     /**
  75.      * @Route("/upsert/{id}", name="api_v3_todo_upsert", defaults={"id": null}, methods={"POST"})
  76.      */
  77.     public function upsert(Request $request, ?int $id = null): Response
  78.     {
  79.         $todo = $this->todoService
  80.             ->setParams($request->request)
  81.             ->upsertTodo($id);
  82.         return $this->todoResponse($todo);
  83.     }
  84.     /**
  85.      * @Route("/done/{id}", name="api_v3_todo_done", methods={"POST"})
  86.      */
  87.     public function done(Request $request, int $id): Response
  88.     {
  89.         $todo = $this->todoService
  90.             ->setParams($request->request)
  91.             ->doneTodo($id);
  92.         return $this->todoResponse($todo);
  93.     }
  94.     /**
  95.      * @Route("/pending-confirmations", name="api_v3_todo_pending_confirmations", methods={"GET"})
  96.      */
  97.     public function pendingConfirmations(): Response
  98.     {
  99.         $todos = $this->todoService->fetchPendingConfirmations();
  100.         return $this->json([
  101.             'todos'    => $this->todoSerialize->setEntity($todos)->setGroups($this->todoSerialize->_groupCore())->render(),
  102.             'severity' => SeverityInterface::SUCCESS,
  103.         ]);
  104.     }
  105.     /**
  106.      * @Route("/confirm/{id}", name="api_v3_todo_confirm", methods={"POST"})
  107.      */
  108.     public function confirm(int $id): Response
  109.     {
  110.         try {
  111.             $todo = $this->todoService->confirmTodo($id);
  112.         } catch (\InvalidArgumentException $e) {
  113.             return $this->json(['message' => $e->getMessage(), 'severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
  114.         }
  115.         return $this->todoResponse($todo);
  116.     }
  117.     /**
  118.      * @Route("/assign-ai/{id}", name="api_v3_todo_assign_ai", methods={"POST"})
  119.      */
  120.     public function assignAi(int $id): Response
  121.     {
  122.         $todo = $this->todoService->toggleAiAssign($id);
  123.         return $this->todoResponse($todo);
  124.     }
  125.     /**
  126.      * @Route("/status/{id}", name="api_v3_todo_change_status", methods={"POST"})
  127.      */
  128.     public function changeStatus(Request $request, int $id): Response
  129.     {
  130.         try {
  131.             $todo = $this->todoService
  132.                 ->setParams($request->request)
  133.                 ->changeStatus($id);
  134.         } catch (\InvalidArgumentException $e) {
  135.             return $this->json(['message' => $e->getMessage(), 'severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
  136.         }
  137.         return $this->todoResponse($todo);
  138.     }
  139.     /**
  140.      * @Route("/ai-queue", name="api_v3_todo_ai_queue", methods={"GET"})
  141.      */
  142.     public function aiQueue(): Response
  143.     {
  144.         $todos = $this->todoService->fetchAiQueue();
  145.         return $this->json([
  146.             'todos'    => $this->todoSerialize->setEntity($todos)->setGroups($this->todoSerialize->_groupCore())->render(),
  147.             'severity' => SeverityInterface::SUCCESS,
  148.         ]);
  149.     }
  150.     /**
  151.      * @Route("/ai-status/{id}", name="api_v3_todo_ai_status", methods={"POST"})
  152.      */
  153.     public function aiStatus(Request $request, int $id): Response
  154.     {
  155.         $todo = $this->todoService
  156.             ->setParams($request->request)
  157.             ->updateAiStatus($id);
  158.         return $this->todoResponse($todo);
  159.     }
  160.     /**
  161.      * @Route("/remove/{id}", name="api_v3_todo_remove", methods={"POST"})
  162.      */
  163.     public function remove(int $id): Response
  164.     {
  165.         $this->todoService->removeTodo($id);
  166.         return $this->json(['severity' => SeverityInterface::SUCCESS]);
  167.     }
  168.     /**
  169.      * Geçerli kullanıcının görülmemiş todo bildirimleri.
  170.      *
  171.      * @Route("/notifications", name="api_v3_todo_notifications", methods={"GET"})
  172.      */
  173.     public function notifications(): Response
  174.     {
  175.         $items = $this->todoService->fetchNotifications();
  176.         $data = array_map(static function (TodoNotification $n) {
  177.             return [
  178.                 'id'         => $n->getId(),
  179.                 'todo_id'    => $n->getTodo() ? $n->getTodo()->getId() : null,
  180.                 'todo_title' => $n->getTodoTitle(),
  181.                 'type'       => $n->getType(),
  182.                 'status'     => $n->getStatus(),
  183.                 'comment_id' => $n->getCommentId(),
  184.                 'note'       => $n->getNote(),
  185.                 'created_at' => $n->getCreatedAt() ? $n->getCreatedAt()->format(DATE_ATOM) : null,
  186.             ];
  187.         }, $items);
  188.         return $this->json([
  189.             'notifications' => array_values($data),
  190.             'severity'      => SeverityInterface::SUCCESS,
  191.         ]);
  192.     }
  193.     /**
  194.      * Bildirimleri görüldü işaretle. `ids` (virgülle ayrık) verilirse yalnız onları,
  195.      * verilmezse kullanıcının tüm görülmemişlerini işaretler.
  196.      *
  197.      * @Route("/notifications/seen", name="api_v3_todo_notifications_seen", methods={"POST"})
  198.      */
  199.     public function notificationsSeen(Request $request): Response
  200.     {
  201.         $this->todoService
  202.             ->setParams($request->request)
  203.             ->markNotificationsSeen();
  204.         return $this->json(['severity' => SeverityInterface::SUCCESS]);
  205.     }
  206.     /**
  207.      * Belirli bir todo'ya ait görülmemiş bildirimleri görüldü işaretle.
  208.      * Drawer açıldığında çağrılır.
  209.      *
  210.      * @Route("/notifications/seen-by-todo/{id}", name="api_v3_todo_notifications_seen_by_todo", methods={"POST"})
  211.      */
  212.     public function notificationsSeenByTodo(int $id): Response
  213.     {
  214.         $this->todoService->markSeenByTodo($id);
  215.         return $this->json(['severity' => SeverityInterface::SUCCESS]);
  216.     }
  217.     /**
  218.      * Anlik ekran goruntusu / belge upload (card-level DropZone). Dosyalar
  219.      * `setFiles()` ile Service'e gecer; Service `new_documents` anahtarini okur.
  220.      *
  221.      * @Route("/document/upload/{id}", name="api_v3_todo_document_upload", methods={"POST"})
  222.      */
  223.     public function documentUpload(Request $request, int $id): Response
  224.     {
  225.         $todo = $this->todoService
  226.             ->setFiles($request->files)
  227.             ->uploadDocuments($id);
  228.         return $this->todoResponse($todo);
  229.     }
  230.     /**
  231.      * Anlik belge silme. Payload: `file_name`.
  232.      *
  233.      * @Route("/document/remove/{id}", name="api_v3_todo_document_remove", methods={"POST"})
  234.      */
  235.     public function documentRemove(Request $request, int $id): Response
  236.     {
  237.         try {
  238.             $todo = $this->todoService
  239.                 ->setParams($request->request)
  240.                 ->removeDocument($id);
  241.         } catch (\InvalidArgumentException $e) {
  242.             return $this->json(['message' => $e->getMessage(), 'severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
  243.         }
  244.         return $this->todoResponse($todo);
  245.     }
  246.     /**
  247.      * Bir todo'nun yorum thread'i. Erisim (sahip/admin) TodoCommentService'te
  248.      * dogrulanir; burada yalnizca FE sekline map'lenir.
  249.      *
  250.      * @Route("/comments/{id}", name="api_v3_todo_comments", methods={"GET"})
  251.      */
  252.     public function comments(int $id, TodoCommentService $commentService): Response
  253.     {
  254.         /** @var User $viewer */
  255.         $viewer   = $this->getUser();
  256.         $comments = $commentService->getThread($id);
  257.         $data = array_map(function (TodoComment $comment) use ($viewer) {
  258.             return $this->shapeComment($comment, $viewer);
  259.         }, $comments);
  260.         return $this->json([
  261.             'comments' => array_values($data),
  262.             'severity' => SeverityInterface::SUCCESS,
  263.         ]);
  264.     }
  265.     /**
  266.      * Yorum ekle. Erisim (sahip veya SUPER_ADMIN) + karsi tarafa bildirim
  267.      * TodoCommentService'te; donen {comment, notified_user_ids} FE'ye map'lenir.
  268.      *
  269.      * @Route("/comments/{id}", name="api_v3_todo_comment_add", methods={"POST"})
  270.      */
  271.     public function addComment(Request $request, int $id, TodoCommentService $commentService): Response
  272.     {
  273.         /** @var User $viewer */
  274.         $viewer = $this->getUser();
  275.         try {
  276.             $result = $commentService
  277.                 ->setParams($request->request)
  278.                 ->createComment($id);
  279.         } catch (\InvalidArgumentException $e) {
  280.             return $this->json(['message' => $e->getMessage(), 'severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
  281.         }
  282.         return $this->json([
  283.             'comment'           => $this->shapeComment($result['comment'], $viewer),
  284.             'notified_user_ids' => array_values($result['notified_user_ids']),
  285.             'severity'          => SeverityInterface::SUCCESS,
  286.         ]);
  287.     }
  288.     /**
  289.      * Bir yorumun ceviri metnini kaydeder. FE once AI ceviri servisini cagirir,
  290.      * sonra bu endpoint ile DB'ye persist eder.
  291.      *
  292.      * @Route("/comment/translate/{id}", name="api_v3_todo_comment_translate", methods={"POST"})
  293.      */
  294.     public function commentTranslate(Request $request, int $id, TodoCommentService $commentService): Response
  295.     {
  296.         $lang           = trim((string) $request->request->get('lang_code', ''));
  297.         $translatedBody = trim((string) $request->request->get('body', ''));
  298.         if ($lang === '' || $translatedBody === '') {
  299.             return $this->json(['message' => 'lang_code and body are required', 'severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
  300.         }
  301.         try {
  302.             $comment = $commentService->saveTranslation($id, $lang, $translatedBody);
  303.         } catch (\InvalidArgumentException $e) {
  304.             return $this->json(['message' => $e->getMessage(), 'severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
  305.         }
  306.         /** @var User $viewer */
  307.         $viewer = $this->getUser();
  308.         return $this->json([
  309.             'comment'  => $this->shapeComment($comment, $viewer),
  310.             'severity' => SeverityInterface::SUCCESS,
  311.         ]);
  312.     }
  313.     /**
  314.      * TodoComment entity'sini FE'nin bekledigi sekle cevirir
  315.      * (types/todo.board.ts -> TodoComment). is_self, goruntuleyen kullaniciya gore.
  316.      */
  317.     private function shapeComment(TodoComment $comment, User $viewer): array
  318.     {
  319.         $author = $comment->getAuthor();
  320.         return [
  321.             'id'              => $comment->getId(),
  322.             'body'            => $comment->getBody(),
  323.             'translated_body' => $comment->getTranslatedBody(),
  324.             'translated_from' => $comment->getTranslatedFrom(),
  325.             'author_id'       => $author ? $author->getId() : null,
  326.             'author'          => $author ? $author->getFullName() : null,
  327.             'is_self'         => $author && $author->getId() === $viewer->getId(),
  328.             'created_at'      => $comment->getCreatedAt() ? $comment->getCreatedAt()->format(DATE_ATOM) : null,
  329.         ];
  330.     }
  331. }