diff --git a/src/Mcp/Tool/Task/CreateTaskRecurrenceTool.php b/src/Mcp/Tool/Task/CreateTaskRecurrenceTool.php new file mode 100644 index 0000000..499e687 --- /dev/null +++ b/src/Mcp/Tool/Task/CreateTaskRecurrenceTool.php @@ -0,0 +1,90 @@ +security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + + $task = $this->taskRepository->find($taskId); + if (null === $task) { + throw new InvalidArgumentException(sprintf('Task with ID %d not found.', $taskId)); + } + + $recurrenceType = RecurrenceType::from($type); + + $recurrence = new TaskRecurrence(); + $recurrence->setType($recurrenceType); + $recurrence->setInterval($interval); + + if (null !== $daysOfWeek) { + $recurrence->setDaysOfWeek($daysOfWeek); + } + if (null !== $dayOfMonth) { + $recurrence->setDayOfMonth($dayOfMonth); + } + if (null !== $weekOfMonth) { + $recurrence->setWeekOfMonth($weekOfMonth); + } + if (null !== $endDate) { + $recurrence->setEndDate(new DateTimeImmutable($endDate)); + } + if (null !== $maxOccurrences) { + $recurrence->setMaxOccurrences($maxOccurrences); + } + + $task->setRecurrence($recurrence); + $this->entityManager->persist($recurrence); + $this->entityManager->flush(); + + $this->calDavService->syncTask($task); + $this->entityManager->flush(); + + return json_encode([ + 'id' => $recurrence->getId(), + 'type' => $recurrence->getType()?->value, + 'interval' => $recurrence->getInterval(), + 'daysOfWeek' => $recurrence->getDaysOfWeek(), + 'dayOfMonth' => $recurrence->getDayOfMonth(), + 'weekOfMonth' => $recurrence->getWeekOfMonth(), + 'endDate' => $recurrence->getEndDate()?->format('Y-m-d'), + 'maxOccurrences' => $recurrence->getMaxOccurrences(), + 'taskId' => $task->getId(), + ]); + } +} diff --git a/src/Mcp/Tool/Task/CreateTaskTool.php b/src/Mcp/Tool/Task/CreateTaskTool.php index 32a7568..3c74934 100644 --- a/src/Mcp/Tool/Task/CreateTaskTool.php +++ b/src/Mcp/Tool/Task/CreateTaskTool.php @@ -14,6 +14,8 @@ use App\Repository\TaskRepository; use App\Repository\TaskStatusRepository; use App\Repository\TaskTagRepository; use App\Repository\UserRepository; +use App\Service\CalDavService; +use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; @@ -36,6 +38,7 @@ class CreateTaskTool private readonly TaskTagRepository $taskTagRepository, private readonly UserRepository $userRepository, private readonly Security $security, + private readonly CalDavService $calDavService, ) {} public function __invoke( @@ -48,6 +51,10 @@ class CreateTaskTool ?int $assigneeId = null, ?int $groupId = null, ?array $tagIds = null, + ?string $scheduledStart = null, + ?string $scheduledEnd = null, + ?string $deadline = null, + ?bool $syncToCalendar = null, ): string { if (!$this->security->isGranted('ROLE_USER')) { throw new AccessDeniedException('Access denied: ROLE_USER required.'); @@ -109,6 +116,18 @@ class CreateTaskTool $task->addTag($tag); } } + if (null !== $scheduledStart) { + $task->setScheduledStart(new DateTimeImmutable($scheduledStart)); + } + if (null !== $scheduledEnd) { + $task->setScheduledEnd(new DateTimeImmutable($scheduledEnd)); + } + if (null !== $deadline) { + $task->setDeadline(new DateTimeImmutable($deadline)); + } + if (null !== $syncToCalendar) { + $task->setSyncToCalendar($syncToCalendar); + } $this->entityManager->wrapInTransaction(function () use ($task, $project): void { $task->setNumber($this->taskRepository->findMaxNumberByProjectForUpdate($project) + 1); @@ -116,19 +135,26 @@ class CreateTaskTool $this->entityManager->flush(); }); + $this->calDavService->syncTask($task); + $this->entityManager->flush(); + return json_encode([ - 'id' => $task->getId(), - 'number' => $task->getNumber(), - 'title' => $task->getTitle(), - 'description' => $task->getDescription(), - 'status' => Serializer::status($task->getStatus()), - 'priority' => Serializer::priority($task->getPriority()), - 'effort' => Serializer::effort($task->getEffort()), - 'assignee' => Serializer::user($task->getAssignee()), - 'group' => Serializer::groupRef($task->getGroup()), - 'project' => Serializer::projectRef($project), - 'tags' => Serializer::tags($task->getTags()), - 'archived' => $task->isArchived(), + 'id' => $task->getId(), + 'number' => $task->getNumber(), + 'title' => $task->getTitle(), + 'description' => $task->getDescription(), + 'status' => Serializer::status($task->getStatus()), + 'priority' => Serializer::priority($task->getPriority()), + 'effort' => Serializer::effort($task->getEffort()), + 'assignee' => Serializer::user($task->getAssignee()), + 'group' => Serializer::groupRef($task->getGroup()), + 'project' => Serializer::projectRef($project), + 'tags' => Serializer::tags($task->getTags()), + 'archived' => $task->isArchived(), + 'scheduledStart' => $task->getScheduledStart()?->format('c'), + 'scheduledEnd' => $task->getScheduledEnd()?->format('c'), + 'deadline' => $task->getDeadline()?->format('c'), + 'syncToCalendar' => $task->isSyncToCalendar(), ]); } } diff --git a/src/Mcp/Tool/Task/DeleteTaskRecurrenceTool.php b/src/Mcp/Tool/Task/DeleteTaskRecurrenceTool.php new file mode 100644 index 0000000..4039fff --- /dev/null +++ b/src/Mcp/Tool/Task/DeleteTaskRecurrenceTool.php @@ -0,0 +1,66 @@ +security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + + $recurrence = $this->taskRecurrenceRepository->find($recurrenceId); + if (null === $recurrence) { + throw new InvalidArgumentException(sprintf('TaskRecurrence with ID %d not found.', $recurrenceId)); + } + + $tasks = $recurrence->getTasks()->toArray(); + + $eventUidToDelete = null; + foreach ($tasks as $task) { + if (null !== $task->getCalendarEventUid()) { + $eventUidToDelete = $task->getCalendarEventUid(); + + break; + } + } + + foreach ($tasks as $task) { + $task->setRecurrence(null); + } + + $this->entityManager->remove($recurrence); + $this->entityManager->flush(); + + if (null !== $eventUidToDelete) { + $this->calDavService->deleteEvent($eventUidToDelete); + } + + return json_encode([ + 'success' => true, + 'message' => sprintf('TaskRecurrence %d deleted.', $recurrenceId), + 'tasksUpdated' => count($tasks), + ]); + } +} diff --git a/src/Mcp/Tool/Task/DeleteTaskTool.php b/src/Mcp/Tool/Task/DeleteTaskTool.php index 7c8644a..fa006a3 100644 --- a/src/Mcp/Tool/Task/DeleteTaskTool.php +++ b/src/Mcp/Tool/Task/DeleteTaskTool.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Mcp\Tool\Task; use App\Repository\TaskRepository; +use App\Service\CalDavService; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; @@ -20,6 +21,7 @@ class DeleteTaskTool private readonly TaskRepository $taskRepository, private readonly EntityManagerInterface $entityManager, private readonly Security $security, + private readonly CalDavService $calDavService, ) {} public function __invoke(int $id): string @@ -35,9 +37,18 @@ class DeleteTaskTool } $taskCode = $task->getProject()->getCode().'-'.$task->getNumber(); + $eventUid = $task->getCalendarEventUid(); + $todoUid = $task->getCalendarTodoUid(); $this->entityManager->remove($task); $this->entityManager->flush(); + if (null !== $eventUid) { + $this->calDavService->deleteEvent($eventUid); + } + if (null !== $todoUid) { + $this->calDavService->deleteTodo($todoUid); + } + return json_encode([ 'success' => true, 'message' => sprintf('Task %s deleted.', $taskCode), diff --git a/src/Mcp/Tool/Task/UpdateTaskRecurrenceTool.php b/src/Mcp/Tool/Task/UpdateTaskRecurrenceTool.php new file mode 100644 index 0000000..ae03b4f --- /dev/null +++ b/src/Mcp/Tool/Task/UpdateTaskRecurrenceTool.php @@ -0,0 +1,88 @@ +security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + + $recurrence = $this->taskRecurrenceRepository->find($recurrenceId); + if (null === $recurrence) { + throw new InvalidArgumentException(sprintf('TaskRecurrence with ID %d not found.', $recurrenceId)); + } + + if (null !== $type) { + $recurrence->setType(RecurrenceType::from($type)); + } + if (null !== $interval) { + $recurrence->setInterval($interval); + } + if (null !== $daysOfWeek) { + $recurrence->setDaysOfWeek($daysOfWeek); + } + if (null !== $dayOfMonth) { + $recurrence->setDayOfMonth($dayOfMonth); + } + if (null !== $weekOfMonth) { + $recurrence->setWeekOfMonth($weekOfMonth); + } + if (null !== $endDate) { + $recurrence->setEndDate(new DateTimeImmutable($endDate)); + } + if (null !== $maxOccurrences) { + $recurrence->setMaxOccurrences($maxOccurrences); + } + + $this->entityManager->flush(); + + foreach ($recurrence->getTasks() as $task) { + $this->calDavService->syncTask($task); + } + $this->entityManager->flush(); + + return json_encode([ + 'id' => $recurrence->getId(), + 'type' => $recurrence->getType()?->value, + 'interval' => $recurrence->getInterval(), + 'daysOfWeek' => $recurrence->getDaysOfWeek(), + 'dayOfMonth' => $recurrence->getDayOfMonth(), + 'weekOfMonth' => $recurrence->getWeekOfMonth(), + 'endDate' => $recurrence->getEndDate()?->format('Y-m-d'), + 'maxOccurrences' => $recurrence->getMaxOccurrences(), + ]); + } +} diff --git a/src/Mcp/Tool/Task/UpdateTaskTool.php b/src/Mcp/Tool/Task/UpdateTaskTool.php index 4f8efbc..ed67b72 100644 --- a/src/Mcp/Tool/Task/UpdateTaskTool.php +++ b/src/Mcp/Tool/Task/UpdateTaskTool.php @@ -12,6 +12,8 @@ use App\Repository\TaskRepository; use App\Repository\TaskStatusRepository; use App\Repository\TaskTagRepository; use App\Repository\UserRepository; +use App\Service\CalDavService; +use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; @@ -33,6 +35,7 @@ class UpdateTaskTool private readonly TaskTagRepository $taskTagRepository, private readonly UserRepository $userRepository, private readonly Security $security, + private readonly CalDavService $calDavService, ) {} public function __invoke( @@ -46,6 +49,10 @@ class UpdateTaskTool ?int $groupId = null, ?array $tagIds = null, ?bool $archived = null, + ?string $scheduledStart = null, + ?string $scheduledEnd = null, + ?string $deadline = null, + ?bool $syncToCalendar = null, ): string { if (!$this->security->isGranted('ROLE_USER')) { throw new AccessDeniedException('Access denied: ROLE_USER required.'); @@ -114,22 +121,40 @@ class UpdateTaskTool if (null !== $archived) { $task->setArchived($archived); } + if (null !== $scheduledStart) { + $task->setScheduledStart(new DateTimeImmutable($scheduledStart)); + } + if (null !== $scheduledEnd) { + $task->setScheduledEnd(new DateTimeImmutable($scheduledEnd)); + } + if (null !== $deadline) { + $task->setDeadline(new DateTimeImmutable($deadline)); + } + if (null !== $syncToCalendar) { + $task->setSyncToCalendar($syncToCalendar); + } + $this->entityManager->flush(); + $this->calDavService->syncTask($task); $this->entityManager->flush(); return json_encode([ - 'id' => $task->getId(), - 'number' => $task->getNumber(), - 'title' => $task->getTitle(), - 'description' => $task->getDescription(), - 'status' => Serializer::status($task->getStatus()), - 'priority' => Serializer::priority($task->getPriority()), - 'effort' => Serializer::effort($task->getEffort()), - 'assignee' => Serializer::user($task->getAssignee()), - 'group' => Serializer::groupRef($task->getGroup()), - 'project' => Serializer::projectRef($task->getProject()), - 'tags' => Serializer::tags($task->getTags()), - 'archived' => $task->isArchived(), + 'id' => $task->getId(), + 'number' => $task->getNumber(), + 'title' => $task->getTitle(), + 'description' => $task->getDescription(), + 'status' => Serializer::status($task->getStatus()), + 'priority' => Serializer::priority($task->getPriority()), + 'effort' => Serializer::effort($task->getEffort()), + 'assignee' => Serializer::user($task->getAssignee()), + 'group' => Serializer::groupRef($task->getGroup()), + 'project' => Serializer::projectRef($task->getProject()), + 'tags' => Serializer::tags($task->getTags()), + 'archived' => $task->isArchived(), + 'scheduledStart' => $task->getScheduledStart()?->format('c'), + 'scheduledEnd' => $task->getScheduledEnd()?->format('c'), + 'deadline' => $task->getDeadline()?->format('c'), + 'syncToCalendar' => $task->isSyncToCalendar(), ]); } }