<?php
namespace App\Controller\Api\v3;
use App\Controller\Api\Service\TodoCommentService;
use App\Controller\Api\Service\TodoService;
use App\Entity\TodoComment;
use App\Entity\TodoNotification;
use App\Entity\Todos;
use App\Entity\User;
use App\Enum\SeverityInterface;
use App\Service\SerializeService\TodoSerialize;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* Todo Board API.
*
* Akis: Controller (request) -> Service (tum process: find + erisim + DB) -> entity
* -> Controller (DI ile alinan Serialize servisi) -> SerializedResponse -> FE
*
* Payload Service'e `setParams($request->request)` / `setFiles($request->files)` ile
* gecer (AbstractControllerService protokolu). Oturumdaki kullanici ve roller
* Service icinde `$this->security`'den okunur — Controller'dan tasinmaz.
*
* @Route("/api/v3/todo")
*/
class TodoController extends AbstractController
{
private TodoService $todoService;
private TodoSerialize $todoSerialize;
private TodoCommentService $commentService;
public function __construct(
TodoService $todoService,
TodoSerialize $todoSerialize,
TodoCommentService $commentService
) {
$this->todoService = $todoService;
$this->todoSerialize = $todoSerialize;
$this->commentService = $commentService;
}
/**
* Tek todo donen endpoint'lerin ortak yaniti — DI ile alinan Serialize
* servisi burada (Controller'da) calisir; Service entity dondurur.
*/
private function todoResponse(Todos $todo): Response
{
$countMap = $this->commentService->countByTodoIds([$todo->getId()]);
$serialized = $this->todoSerialize->setEntity($todo)->setGroups($this->todoSerialize->_groupCore())->render();
$serialized['comment_count'] = $countMap[$todo->getId()] ?? 0;
return $this->json([
'todo' => $serialized,
'notified_admin_ids' => array_values($this->todoService->getLastNotifiedAdminIds()),
'severity' => SeverityInterface::SUCCESS,
]);
}
/**
* @Route("/todos", name="api_v3_todo_todos", methods={"GET"})
*/
public function todos(): Response
{
$todos = $this->todoService->fetchTodos();
$ids = array_map(function (Todos $t) { return $t->getId(); }, $todos);
$countMap = $this->commentService->countByTodoIds($ids);
$serialized = $this->todoSerialize->setEntity($todos)->setGroups($this->todoSerialize->_groupCore())->render();
foreach ($serialized as &$item) {
$item['comment_count'] = $countMap[$item['id']] ?? 0;
}
unset($item);
return $this->json([
'todos' => array_values($serialized),
'severity' => SeverityInterface::SUCCESS,
]);
}
/**
* @Route("/upsert/{id}", name="api_v3_todo_upsert", defaults={"id": null}, methods={"POST"})
*/
public function upsert(Request $request, ?int $id = null): Response
{
$todo = $this->todoService
->setParams($request->request)
->upsertTodo($id);
return $this->todoResponse($todo);
}
/**
* @Route("/done/{id}", name="api_v3_todo_done", methods={"POST"})
*/
public function done(Request $request, int $id): Response
{
$todo = $this->todoService
->setParams($request->request)
->doneTodo($id);
return $this->todoResponse($todo);
}
/**
* @Route("/pending-confirmations", name="api_v3_todo_pending_confirmations", methods={"GET"})
*/
public function pendingConfirmations(): Response
{
$todos = $this->todoService->fetchPendingConfirmations();
return $this->json([
'todos' => $this->todoSerialize->setEntity($todos)->setGroups($this->todoSerialize->_groupCore())->render(),
'severity' => SeverityInterface::SUCCESS,
]);
}
/**
* @Route("/confirm/{id}", name="api_v3_todo_confirm", methods={"POST"})
*/
public function confirm(int $id): Response
{
try {
$todo = $this->todoService->confirmTodo($id);
} catch (\InvalidArgumentException $e) {
return $this->json(['message' => $e->getMessage(), 'severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
}
return $this->todoResponse($todo);
}
/**
* @Route("/assign-ai/{id}", name="api_v3_todo_assign_ai", methods={"POST"})
*/
public function assignAi(int $id): Response
{
$todo = $this->todoService->toggleAiAssign($id);
return $this->todoResponse($todo);
}
/**
* @Route("/status/{id}", name="api_v3_todo_change_status", methods={"POST"})
*/
public function changeStatus(Request $request, int $id): Response
{
try {
$todo = $this->todoService
->setParams($request->request)
->changeStatus($id);
} catch (\InvalidArgumentException $e) {
return $this->json(['message' => $e->getMessage(), 'severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
}
return $this->todoResponse($todo);
}
/**
* @Route("/ai-queue", name="api_v3_todo_ai_queue", methods={"GET"})
*/
public function aiQueue(): Response
{
$todos = $this->todoService->fetchAiQueue();
return $this->json([
'todos' => $this->todoSerialize->setEntity($todos)->setGroups($this->todoSerialize->_groupCore())->render(),
'severity' => SeverityInterface::SUCCESS,
]);
}
/**
* @Route("/ai-status/{id}", name="api_v3_todo_ai_status", methods={"POST"})
*/
public function aiStatus(Request $request, int $id): Response
{
$todo = $this->todoService
->setParams($request->request)
->updateAiStatus($id);
return $this->todoResponse($todo);
}
/**
* @Route("/remove/{id}", name="api_v3_todo_remove", methods={"POST"})
*/
public function remove(int $id): Response
{
$this->todoService->removeTodo($id);
return $this->json(['severity' => SeverityInterface::SUCCESS]);
}
/**
* Geçerli kullanıcının görülmemiş todo bildirimleri.
*
* @Route("/notifications", name="api_v3_todo_notifications", methods={"GET"})
*/
public function notifications(): Response
{
$items = $this->todoService->fetchNotifications();
$data = array_map(static function (TodoNotification $n) {
return [
'id' => $n->getId(),
'todo_id' => $n->getTodo() ? $n->getTodo()->getId() : null,
'todo_title' => $n->getTodoTitle(),
'type' => $n->getType(),
'status' => $n->getStatus(),
'comment_id' => $n->getCommentId(),
'note' => $n->getNote(),
'created_at' => $n->getCreatedAt() ? $n->getCreatedAt()->format(DATE_ATOM) : null,
];
}, $items);
return $this->json([
'notifications' => array_values($data),
'severity' => SeverityInterface::SUCCESS,
]);
}
/**
* Bildirimleri görüldü işaretle. `ids` (virgülle ayrık) verilirse yalnız onları,
* verilmezse kullanıcının tüm görülmemişlerini işaretler.
*
* @Route("/notifications/seen", name="api_v3_todo_notifications_seen", methods={"POST"})
*/
public function notificationsSeen(Request $request): Response
{
$this->todoService
->setParams($request->request)
->markNotificationsSeen();
return $this->json(['severity' => SeverityInterface::SUCCESS]);
}
/**
* Belirli bir todo'ya ait görülmemiş bildirimleri görüldü işaretle.
* Drawer açıldığında çağrılır.
*
* @Route("/notifications/seen-by-todo/{id}", name="api_v3_todo_notifications_seen_by_todo", methods={"POST"})
*/
public function notificationsSeenByTodo(int $id): Response
{
$this->todoService->markSeenByTodo($id);
return $this->json(['severity' => SeverityInterface::SUCCESS]);
}
/**
* Anlik ekran goruntusu / belge upload (card-level DropZone). Dosyalar
* `setFiles()` ile Service'e gecer; Service `new_documents` anahtarini okur.
*
* @Route("/document/upload/{id}", name="api_v3_todo_document_upload", methods={"POST"})
*/
public function documentUpload(Request $request, int $id): Response
{
$todo = $this->todoService
->setFiles($request->files)
->uploadDocuments($id);
return $this->todoResponse($todo);
}
/**
* Anlik belge silme. Payload: `file_name`.
*
* @Route("/document/remove/{id}", name="api_v3_todo_document_remove", methods={"POST"})
*/
public function documentRemove(Request $request, int $id): Response
{
try {
$todo = $this->todoService
->setParams($request->request)
->removeDocument($id);
} catch (\InvalidArgumentException $e) {
return $this->json(['message' => $e->getMessage(), 'severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
}
return $this->todoResponse($todo);
}
/**
* Bir todo'nun yorum thread'i. Erisim (sahip/admin) TodoCommentService'te
* dogrulanir; burada yalnizca FE sekline map'lenir.
*
* @Route("/comments/{id}", name="api_v3_todo_comments", methods={"GET"})
*/
public function comments(int $id, TodoCommentService $commentService): Response
{
/** @var User $viewer */
$viewer = $this->getUser();
$comments = $commentService->getThread($id);
$data = array_map(function (TodoComment $comment) use ($viewer) {
return $this->shapeComment($comment, $viewer);
}, $comments);
return $this->json([
'comments' => array_values($data),
'severity' => SeverityInterface::SUCCESS,
]);
}
/**
* Yorum ekle. Erisim (sahip veya SUPER_ADMIN) + karsi tarafa bildirim
* TodoCommentService'te; donen {comment, notified_user_ids} FE'ye map'lenir.
*
* @Route("/comments/{id}", name="api_v3_todo_comment_add", methods={"POST"})
*/
public function addComment(Request $request, int $id, TodoCommentService $commentService): Response
{
/** @var User $viewer */
$viewer = $this->getUser();
try {
$result = $commentService
->setParams($request->request)
->createComment($id);
} catch (\InvalidArgumentException $e) {
return $this->json(['message' => $e->getMessage(), 'severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
}
return $this->json([
'comment' => $this->shapeComment($result['comment'], $viewer),
'notified_user_ids' => array_values($result['notified_user_ids']),
'severity' => SeverityInterface::SUCCESS,
]);
}
/**
* Bir yorumun ceviri metnini kaydeder. FE once AI ceviri servisini cagirir,
* sonra bu endpoint ile DB'ye persist eder.
*
* @Route("/comment/translate/{id}", name="api_v3_todo_comment_translate", methods={"POST"})
*/
public function commentTranslate(Request $request, int $id, TodoCommentService $commentService): Response
{
$lang = trim((string) $request->request->get('lang_code', ''));
$translatedBody = trim((string) $request->request->get('body', ''));
if ($lang === '' || $translatedBody === '') {
return $this->json(['message' => 'lang_code and body are required', 'severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
}
try {
$comment = $commentService->saveTranslation($id, $lang, $translatedBody);
} catch (\InvalidArgumentException $e) {
return $this->json(['message' => $e->getMessage(), 'severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
}
/** @var User $viewer */
$viewer = $this->getUser();
return $this->json([
'comment' => $this->shapeComment($comment, $viewer),
'severity' => SeverityInterface::SUCCESS,
]);
}
/**
* TodoComment entity'sini FE'nin bekledigi sekle cevirir
* (types/todo.board.ts -> TodoComment). is_self, goruntuleyen kullaniciya gore.
*/
private function shapeComment(TodoComment $comment, User $viewer): array
{
$author = $comment->getAuthor();
return [
'id' => $comment->getId(),
'body' => $comment->getBody(),
'translated_body' => $comment->getTranslatedBody(),
'translated_from' => $comment->getTranslatedFrom(),
'author_id' => $author ? $author->getId() : null,
'author' => $author ? $author->getFullName() : null,
'is_self' => $author && $author->getId() === $viewer->getId(),
'created_at' => $comment->getCreatedAt() ? $comment->getCreatedAt()->format(DATE_ATOM) : null,
];
}
}