diff --git a/src/Repository/TaskStatusRepository.php b/src/Repository/TaskStatusRepository.php index b6cc26d..fdc80b5 100644 --- a/src/Repository/TaskStatusRepository.php +++ b/src/Repository/TaskStatusRepository.php @@ -14,4 +14,16 @@ class TaskStatusRepository extends ServiceEntityRepository { parent::__construct($registry, TaskStatus::class); } + + public function findFirstNonFinal(): ?TaskStatus + { + return $this->createQueryBuilder('s') + ->where('s.isFinal = :final') + ->setParameter('final', false) + ->orderBy('s.position', 'ASC') + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult() + ; + } } diff --git a/src/State/RecurrenceHandler.php b/src/State/RecurrenceHandler.php new file mode 100644 index 0000000..b6831b2 --- /dev/null +++ b/src/State/RecurrenceHandler.php @@ -0,0 +1,105 @@ +getStatus(); + $isNowFinal = $currentStatus?->isFinal() ?? false; + + if (!$isNowFinal || $wasAlreadyFinal) { + return; // No transition to final + } + + $recurrence = $task->getRecurrence(); + if (null === $recurrence) { + return; // Not a recurring task + } + + if ($this->calculator->hasReachedEnd($recurrence)) { + return; // Recurrence is done + } + + $nextStart = $this->calculator->getNextDate($task); + if (null === $nextStart) { + return; + } + + // Archive current task, clear calendar UIDs + $savedEventUid = $task->getCalendarEventUid(); + $task->setArchived(true); + $task->setCalendarEventUid(null); + $task->setCalendarTodoUid(null); + + // Create new task with same fields + $newTask = new Task(); + $newTask->setProject($task->getProject()); + $newTask->setTitle($task->getTitle()); + $newTask->setDescription($task->getDescription()); + $newTask->setAssignee($task->getAssignee()); + $newTask->setEffort($task->getEffort()); + $newTask->setPriority($task->getPriority()); + $newTask->setGroup($task->getGroup()); + $newTask->setRecurrence($recurrence); + $newTask->setSyncToCalendar($task->isSyncToCalendar()); + + // Copy tags + foreach ($task->getTags() as $tag) { + $newTask->addTag($tag); + } + + // Set first non-final status + $firstStatus = $this->statusRepository->findFirstNonFinal(); + $newTask->setStatus($firstStatus); + + // Set recalculated dates + $newTask->setScheduledStart($nextStart); + $newTask->setScheduledEnd($this->calculator->getNextEnd($task, $nextStart)); + $newTask->setDeadline($this->calculator->getNextDeadline($task, $nextStart)); + + // Copy calendar event UID (recurring VEVENT is shared) + $newTask->setCalendarEventUid($savedEventUid); + + // Generate task number in transaction + $this->entityManager->wrapInTransaction(function () use ($newTask): void { + $maxNumber = $this->taskRepository->findMaxNumberByProjectForUpdate($newTask->getProject()); + $newTask->setNumber($maxNumber + 1); + $this->entityManager->persist($newTask); + $this->entityManager->flush(); + }); + + // Increment occurrence count (with optimistic locking via @Version) + $recurrence->incrementOccurrenceCount(); + $this->entityManager->flush(); + + // Sync new task's VTODO (new deadline) to Zimbra + if ($newTask->isSyncToCalendar() && $newTask->getDeadline()) { + $uid = $this->calDavService->createTodo($newTask); + if ($uid) { + $newTask->setCalendarTodoUid($uid); + $newTask->setCalendarSyncError(null); + $this->entityManager->flush(); + } + } + } +}