src/Controller/Api/v3/TaskExecutionController.php line 357

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Api\v3;
  3. use App\Controller\Api\Service\TaskExecutionService;
  4. use App\Entity\ProjectOrderTaskFulfillment;
  5. use App\Entity\ProjectOrderTasks;
  6. use App\Enum\ReferenceEnum;
  7. use App\Enum\SeverityInterface;
  8. use App\Service\SerializeService\BranchSerialize;
  9. use App\Service\SerializeService\OrderTaskFulfillmentSerialize;
  10. use App\Service\SerializeService\ProjectOrderTaskFulfillmentMessageSerialize;
  11. use App\Service\SerializeService\ReferenceSerialize;
  12. use App\Service\SerializeService\VendorSerialize;
  13. use App\Service\StreamingService;
  14. use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
  15. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  16. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  17. use Symfony\Component\HttpFoundation\BinaryFileResponse;
  18. use Symfony\Component\HttpFoundation\Request;
  19. use Symfony\Component\HttpFoundation\Response;
  20. use Symfony\Component\HttpFoundation\ResponseHeaderBag;
  21. use Symfony\Component\HttpFoundation\StreamedResponse;
  22. use Symfony\Component\Routing\Annotation\Route;
  23. use Symfony\Contracts\Translation\TranslatorInterface;
  24. /**
  25.  * TaskExecution — Order Task → [Execute] → Servise devret (ERP tarafı, oturumlu
  26.  * kullanıcı). Ağ içi (X-Internal-Token) uçlar için bkz. TaskExecutionInternalController.
  27.  * Eski Fulfillment akışına (OrderTaskFulfillmentController) DOKUNULMADI.
  28.  *
  29.  * Yanıt sözleşmesi te-frontend ile 2026-08-10'da donduruldu (bkz. FE
  30.  * `Tasks/types/task.execution.types.ts` + `hooks/useTaskExecution.tsx`):
  31.  *   - execution/upsert/publish yanıtlarında entity anahtarı **`execution`** (taskExecution DEĞİL)
  32.  *   - `open` yanıtı execution'ın YANINDA `branches`/`vendor`/`reasons`'ı da BUNDLE eder
  33.  *     (FE `needed-collections`'ı bu turda ayrıca ÇAĞIRMIYOR — open tek seferde yeterli olsun diye)
  34.  *
  35.  * @Route("/api/v3/task-execution")
  36.  */
  37. class TaskExecutionController extends AbstractController
  38. {
  39.     /**
  40.      * K4 — find-or-create draft. execution + wizard'ın ihtiyaç duyduğu master-data
  41.      * (branches/vendor/reasons) TEK yanıtta döner (FE sözleşmesi).
  42.      *
  43.      * @Route("/open/{task_id}", name="api_v3_task_execution_open", methods={"POST"})
  44.      * @ParamConverter("task", options={"mapping": {"task_id": "id"}})
  45.      * @IsGranted("SECTION:order:task:u")
  46.      */
  47.     public function open(
  48.         ProjectOrderTasks $task,
  49.         TranslatorInterface $translator,
  50.         TaskExecutionService $service,
  51.         OrderTaskFulfillmentSerialize $serialize,
  52.         BranchSerialize $branchSerialize,
  53.         VendorSerialize $vendorSerialize,
  54.         ReferenceSerialize $referenceSerialize
  55.     ): Response {
  56.         try {
  57.             $execution $service->open($task);
  58.         } catch (\DomainException $e) {
  59.             // İş kuralı reddi (ownership) — 403 KULLANMA: FE'de HttpRequest 403'ü "oturum düştü"
  60.             // sayıp window.location.reload() yapıyor (HttpRequest.tsx:189). upsert/publish ile
  61.             // tutarlı: 409 → FE snackbar gösterir, sayfa reload OLMAZ.
  62.             return $this->json([
  63.                 'message'  => $translator->trans($e->getMessage()),
  64.                 'severity' => SeverityInterface::ERROR,
  65.             ], Response::HTTP_CONFLICT);
  66.         }
  67.         return $this->json([
  68.             'message'   => $translator->trans('Task execution ready'),
  69.             'severity'  => SeverityInterface::SUCCESS,
  70.             // serializeFullPreview — SummaryView (published/readOnly ekran) progress ring + job
  71.             // breakdown çizsin diye execution_progress (@progress) + task.task_jobs[] taşır.
  72.             'taskExecution' => $serialize->setEntity($execution)->serializeFullPreview(),
  73.             'branches'  => $this->serializeBranches($service$branchSerialize),
  74.             'vendor'    => $vendorSerialize->setEntity($service->fetchActiveVendors())->serializeFull(),
  75.             'reasons'   => $referenceSerialize->setEntity($service->fetchAllReasons())->serializeCustomCore(ReferenceEnum::TASK_FULFILLMENT_REASON),
  76.             // Layout B "canlı brief" eklentisi — order'a şimdiye kadar yüklenen/harcanan maliyet
  77.             // (EUR). Kaynak: project_order_metrics.spent_costs, SADECE READ. Şema/hesap DEĞİŞMEDİ.
  78.             'order_spent' => $service->getOrderSpentCost($task),
  79.         ], Response::HTTP_OK);
  80.     }
  81.     /**
  82.      * @Route("/upsert/{id}", name="api_v3_task_execution_upsert", methods={"POST"})
  83.      * @ParamConverter("execution", options={"mapping": {"id": "id"}})
  84.      * @IsGranted("SECTION:order:task:u")
  85.      */
  86.     public function upsert(
  87.         ?ProjectOrderTaskFulfillment $execution,
  88.         Request $request,
  89.         TranslatorInterface $translator,
  90.         TaskExecutionService $service,
  91.         OrderTaskFulfillmentSerialize $serialize
  92.     ): Response {
  93.         if (!$execution) {
  94.             return $this->json([
  95.                 'message'  => $translator->trans('Task execution not found'),
  96.                 'severity' => SeverityInterface::ERROR,
  97.             ], Response::HTTP_NOT_FOUND);
  98.         }
  99.         // work_documents dosyalari icin AbstractControllerService protokolu ($this->files).
  100.         $service->setFiles($request->files);
  101.         try {
  102.             $execution $service->upsert($execution$request->request);
  103.         } catch (\DomainException $e) {
  104.             return $this->json([
  105.                 'message'  => $translator->trans($e->getMessage()),
  106.                 'severity' => SeverityInterface::ERROR,
  107.             ], Response::HTTP_CONFLICT);
  108.         }
  109.         return $this->json([
  110.             'message'   => $translator->trans('Task execution updated successfully'),
  111.             'severity'  => SeverityInterface::SUCCESS,
  112.             'taskExecution' => $serialize->setEntity($execution)->serializeFull(),
  113.         ], Response::HTTP_OK);
  114.     }
  115.     /**
  116.      * K7 — yayin SONRASI dokuman ekle/sil. task/order owner published bir execution'a bile
  117.      * is-plani dokumani ekleyebilir (K5 istisnasi — dokuman ilerleme degil). Yalniz work_documents
  118.      * islenir. Ownership ihlali → 409 (403 KULLANMA, FE reload eder).
  119.      *
  120.      * @Route("/documents/{id}", name="api_v3_task_execution_documents", methods={"POST"})
  121.      * @ParamConverter("execution", options={"mapping": {"id": "id"}})
  122.      * @IsGranted("SECTION:order:task:u")
  123.      */
  124.     public function uploadDocuments(
  125.         ?ProjectOrderTaskFulfillment $execution,
  126.         Request $request,
  127.         TranslatorInterface $translator,
  128.         TaskExecutionService $service,
  129.         OrderTaskFulfillmentSerialize $serialize
  130.     ): Response {
  131.         if (!$execution) {
  132.             return $this->json([
  133.                 'message'  => $translator->trans('Task execution not found'),
  134.                 'severity' => SeverityInterface::ERROR,
  135.             ], Response::HTTP_NOT_FOUND);
  136.         }
  137.         $service->setFiles($request->files);
  138.         try {
  139.             $execution $service->syncExecutionDocuments($execution$request->request);
  140.         } catch (\DomainException $e) {
  141.             return $this->json([
  142.                 'message'  => $translator->trans($e->getMessage()),
  143.                 'severity' => SeverityInterface::ERROR,
  144.             ], Response::HTTP_CONFLICT);
  145.         }
  146.         return $this->json([
  147.             'message'       => $translator->trans('Documents updated successfully'),
  148.             'severity'      => SeverityInterface::SUCCESS,
  149.             // K7 dokuman degisimi published/readOnly ekranda olur — progress ring + task_jobs
  150.             // aci kalmasin diye open ile ayni serializeFullPreview kullanilir.
  151.             'taskExecution' => $serialize->setEntity($execution)->serializeFullPreview(),
  152.         ], Response::HTTP_OK);
  153.     }
  154.     /**
  155.      * K5 — yayınla. K3/K4 zorunlu alanları eksikse 422, zaten yayınlıysa 409.
  156.      *
  157.      * @Route("/publish/{id}", name="api_v3_task_execution_publish", methods={"POST"})
  158.      * @ParamConverter("execution", options={"mapping": {"id": "id"}})
  159.      * @IsGranted("SECTION:order:task:u")
  160.      */
  161.     public function publish(
  162.         ?ProjectOrderTaskFulfillment $execution,
  163.         TranslatorInterface $translator,
  164.         TaskExecutionService $service,
  165.         OrderTaskFulfillmentSerialize $serialize
  166.     ): Response {
  167.         if (!$execution) {
  168.             return $this->json([
  169.                 'message'  => $translator->trans('Task execution not found'),
  170.                 'severity' => SeverityInterface::ERROR,
  171.             ], Response::HTTP_NOT_FOUND);
  172.         }
  173.         try {
  174.             $execution $service->publish($execution);
  175.         } catch (\DomainException $e) {
  176.             $alreadyPublished strpos($e->getMessage(), 'already published') !== false;
  177.             return $this->json([
  178.                 'message'  => $translator->trans($e->getMessage()),
  179.                 'severity' => SeverityInterface::ERROR,
  180.             ], $alreadyPublished Response::HTTP_CONFLICT Response::HTTP_UNPROCESSABLE_ENTITY);
  181.         }
  182.         return $this->json([
  183.             'message'   => $translator->trans('Task execution published successfully'),
  184.             'severity'  => SeverityInterface::SUCCESS,
  185.             'taskExecution' => $serialize->setEntity($execution)->serializeFull(),
  186.         ], Response::HTTP_OK);
  187.     }
  188.     /**
  189.      * K9 — GERİ ÇEK (rollback) + TAM SIFIRLA. PENDING/DECLINED (yayınlanmış ama KABUL EDİLMEMİŞ)
  190.      * execution'ı kayıt + fiziksel dosyalarıyla TAMAMEN siler → "hiç execution olmamış" hale
  191.      * (taslağa döndürme DEĞİL). ACCEPTED ise 409. FE bu ucu bir confirmation dialog'unun arkasına koyar.
  192.      *
  193.      * @Route("/rollback/{id}", name="api_v3_task_execution_rollback", methods={"POST"})
  194.      * @ParamConverter("execution", options={"mapping": {"id": "id"}})
  195.      * @IsGranted("SECTION:order:task:u")
  196.      */
  197.     public function rollback(
  198.         ?ProjectOrderTaskFulfillment $execution,
  199.         TranslatorInterface $translator,
  200.         TaskExecutionService $service
  201.     ): Response {
  202.         if (!$execution) {
  203.             return $this->json([
  204.                 'message'  => $translator->trans('Task execution not found'),
  205.                 'severity' => SeverityInterface::ERROR,
  206.             ], Response::HTTP_NOT_FOUND);
  207.         }
  208.         try {
  209.             $service->rollbackAndReset($execution);
  210.         } catch (\DomainException $e) {
  211.             return $this->json([
  212.                 'message'  => $translator->trans($e->getMessage()),
  213.                 'severity' => SeverityInterface::ERROR,
  214.             ], Response::HTTP_CONFLICT);
  215.         }
  216.         return $this->json([
  217.             'message'  => $translator->trans('Task execution rolled back'),
  218.             'severity' => SeverityInterface::SUCCESS,
  219.         ], Response::HTTP_OK);
  220.     }
  221.     /**
  222.      * Chat-K1/K2 (karar 59) — OWNER mesaj LİSTESİ + iki taraflı unread sayaç. Mesaj İÇERİĞİ NS'ten
  223.      * GEÇMEZ (Chat-K1) — bu uç kimlik-doğrulamalı (session) tek kaynaktır. `serializeFullWithJobs`/
  224.      * `serializeFullPreview` payload'ına DOKUNULMADI, mesajlar execution objesine ENJEKTE EDİLMEZ.
  225.      *
  226.      * @Route("/message/list/{execution_id}", name="api_v3_task_execution_message_list", methods={"POST"})
  227.      * @ParamConverter("execution", options={"mapping": {"execution_id": "id"}})
  228.      * @IsGranted("SECTION:order:task:r")
  229.      */
  230.     public function messageList(
  231.         ?ProjectOrderTaskFulfillment $execution,
  232.         TranslatorInterface $translator,
  233.         TaskExecutionService $service
  234.     ): Response {
  235.         if (!$execution) {
  236.             return $this->json([
  237.                 'message'  => $translator->trans('Task execution not found'),
  238.                 'severity' => SeverityInterface::ERROR,
  239.             ], Response::HTTP_NOT_FOUND);
  240.         }
  241.         try {
  242.             $payload $service->listMessages($execution);
  243.         } catch (\DomainException $e) {
  244.             return $this->json([
  245.                 'message'  => $translator->trans($e->getMessage()),
  246.                 'severity' => SeverityInterface::ERROR,
  247.             ], Response::HTTP_CONFLICT);
  248.         }
  249.         return $this->json($payloadResponse::HTTP_OK);
  250.     }
  251.     /**
  252.      * Chat-K1 — OWNER mesaj GÖNDER. Body: `message` (ZORUNLU, boş → 422). sender_side=owner,
  253.      * sender_name = currentActor() (WorkerActivities→Worker) full_name. NS EMIT BURADA YAPILMAZ —
  254.      * FE emit eder (Chat-K1: içerik NS'ten geçmez, yalnız sinyal).
  255.      *
  256.      * @Route("/message/send/{execution_id}", name="api_v3_task_execution_message_send", methods={"POST"})
  257.      * @ParamConverter("execution", options={"mapping": {"execution_id": "id"}})
  258.      * @IsGranted("SECTION:order:task:u")
  259.      */
  260.     public function messageSend(
  261.         ?ProjectOrderTaskFulfillment $execution,
  262.         Request $request,
  263.         TranslatorInterface $translator,
  264.         TaskExecutionService $service,
  265.         ProjectOrderTaskFulfillmentMessageSerialize $messageSerialize
  266.     ): Response {
  267.         if (!$execution) {
  268.             return $this->json([
  269.                 'message'  => $translator->trans('Task execution not found'),
  270.                 'severity' => SeverityInterface::ERROR,
  271.             ], Response::HTTP_NOT_FOUND);
  272.         }
  273.         try {
  274.             $message $service->sendMessage($execution$request->request->get('message'));
  275.         } catch (\DomainException $e) {
  276.             return $this->json([
  277.                 'message'  => $translator->trans($e->getMessage()),
  278.                 'severity' => SeverityInterface::ERROR,
  279.             ], Response::HTTP_UNPROCESSABLE_ENTITY);
  280.         }
  281.         return $this->json([
  282.             'message'      => $translator->trans('Message sent'),
  283.             'severity'     => SeverityInterface::SUCCESS,
  284.             'chat_message' => $messageSerialize->setEntity($message)->serializeCore(),
  285.         ], Response::HTTP_OK);
  286.     }
  287.     /**
  288.      * Chat-K2 — OWNER "okundu" işaretle. owner_last_read_at = now.
  289.      *
  290.      * @Route("/message/read/{execution_id}", name="api_v3_task_execution_message_read", methods={"POST"})
  291.      * @ParamConverter("execution", options={"mapping": {"execution_id": "id"}})
  292.      * @IsGranted("SECTION:order:task:u")
  293.      */
  294.     public function messageRead(
  295.         ?ProjectOrderTaskFulfillment $execution,
  296.         TranslatorInterface $translator,
  297.         TaskExecutionService $service
  298.     ): Response {
  299.         if (!$execution) {
  300.             return $this->json([
  301.                 'message'  => $translator->trans('Task execution not found'),
  302.                 'severity' => SeverityInterface::ERROR,
  303.             ], Response::HTTP_NOT_FOUND);
  304.         }
  305.         try {
  306.             $service->markMessagesRead($execution);
  307.         } catch (\DomainException $e) {
  308.             return $this->json([
  309.                 'message'  => $translator->trans($e->getMessage()),
  310.                 'severity' => SeverityInterface::ERROR,
  311.             ], Response::HTTP_CONFLICT);
  312.         }
  313.         return $this->json([
  314.             'message'  => $translator->trans('Messages marked as read'),
  315.             'severity' => SeverityInterface::SUCCESS,
  316.         ], Response::HTTP_OK);
  317.     }
  318.     /**
  319.      * App-level GLOBAL mesaj bildirimi — oturumdaki order lead'in TÜM YAYINLANMIŞ execution'larını
  320.      * NS oda anahtarlarıyla (`scope` + `contact_person_id` → `te:<scope>:<contactPersonId>`, T58)
  321.      * döner. FE bu listeyle TÜM odalara abone olur, `task_execution_message` sinyalinde snackbar
  322.      * gösterir ("{task_title} için yeni mesaj"). Owner filtresi Service'te (assertOwnership'in LİSTE
  323.      * hali) — FE'de gizlemek YETMEZ.
  324.      *
  325.      * @Route("/owner/executions", name="api_v3_task_execution_owner_executions", methods={"POST"})
  326.      * @IsGranted("SECTION:order:task:r")
  327.      */
  328.     public function ownerExecutions(TaskExecutionService $service): Response
  329.     {
  330.         return $this->json($service->ownerExecutionRooms(), Response::HTTP_OK);
  331.     }
  332.     /**
  333.      * "Liste Click Preview" — ERP owner iş planı dokümanını ÖNİZLER (inline stream). Session-auth;
  334.      * ownership Service::resolveOwnedWorkDocument'te (order lead) — başka order'ın dokümanı sızmaz.
  335.      * Okuma yetkisi (task:r) yeter; salt görüntüleme.
  336.      *
  337.      * @Route("/work-document/{id}", name="api_v3_task_execution_work_document", methods={"GET"})
  338.      * @IsGranted("SECTION:order:task:r")
  339.      */
  340.     public function workDocument(
  341.         int $id,
  342.         TaskExecutionService $service,
  343.         TranslatorInterface $translator
  344.     ): Response {
  345.         try {
  346.             $resolved $service->resolveOwnedWorkDocument($id);
  347.         } catch (\DomainException $e) {
  348.             return $this->json([
  349.                 'message'  => $translator->trans($e->getMessage()),
  350.                 'severity' => SeverityInterface::ERROR,
  351.             ], Response::HTTP_UNPROCESSABLE_ENTITY);
  352.         }
  353.         $response = new BinaryFileResponse($resolved['path']);
  354.         $response->headers->set('Content-Type'$resolved['mime']);
  355.         $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE$resolved['name']);
  356.         return $response;
  357.     }
  358.     /**
  359.      * Wizard'in ihtiyaç duyduğu 3 koleksiyon ayrı ayrı (streamed). `open` bunları zaten
  360.      * bundle ettiği için FE bu turda bu ucu ÇAĞIRMIYOR — sözleşmede kayıtlı dursun diye var
  361.      * (ör. dialog dışı bir ekran ileride ihtiyaç duyarsa).
  362.      *
  363.      * @Route("/needed-collections", name="api_v3_task_execution_needed_collections")
  364.      * @IsGranted("SECTION:order:task:u")
  365.      */
  366.     public function neededCollections(
  367.         TaskExecutionService $service,
  368.         TranslatorInterface $translator,
  369.         BranchSerialize $branchSerialize,
  370.         VendorSerialize $vendorSerialize,
  371.         ReferenceSerialize $referenceSerialize
  372.     ): StreamedResponse {
  373.         $streamingService = new StreamingService([
  374.             'localText' => ['completed' => $translator->trans('Completed')],
  375.         ]);
  376.         $streamingService->appendStream(
  377.             'branches',
  378.             'Branches loaded',
  379.             function () use ($service$branchSerialize) {
  380.                 return $this->serializeBranches($service$branchSerialize);
  381.             }
  382.         );
  383.         $streamingService->appendStream(
  384.             'vendors',
  385.             'Active vendors loaded',
  386.             function () use ($service$vendorSerialize) {
  387.                 return $vendorSerialize->setEntity($service->fetchActiveVendors())->serializeFull();
  388.             }
  389.         );
  390.         $streamingService->appendStream(
  391.             'reasons',
  392.             'Reasons loaded',
  393.             function () use ($service$referenceSerialize) {
  394.                 return $referenceSerialize->setEntity($service->fetchAllReasons())->serializeCustomCore(ReferenceEnum::TASK_FULFILLMENT_REASON);
  395.             }
  396.         );
  397.         // Layout B "canlı brief" eklentisi — kişi başına AKTİF execution sayısı ("N aktif iş").
  398.         // {branch_contact:{[workerActivityId]:n}, vendor_contact:{[vendorContactPersonId]:n}}
  399.         $streamingService->appendStream(
  400.             'active_task_counts',
  401.             'Active task counts loaded',
  402.             function () use ($service) {
  403.                 return $service->fetchActiveTaskCounts();
  404.             }
  405.         );
  406.         return $streamingService->flush();
  407.     }
  408.     private function serializeBranches(TaskExecutionService $serviceBranchSerialize $branchSerialize): ?array
  409.     {
  410.         return $branchSerialize->setEntity($service->fetchAllBranches())->setGroups([
  411.             'branch@core',
  412.             'branch@responsible',
  413.             'worker_activity@core',
  414.             'worker_activity@worker',
  415.             'worker@core',
  416.         ])->render();
  417.     }
  418. }