feat : add RecurrenceHandler for auto-creating next recurring task
When a task transitions to a final status, archives the current task and creates a new occurrence with recalculated dates. Adds TaskStatusRepository::findFirstNonFinal() to assign the initial status to the new task. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
105
src/State/RecurrenceHandler.php
Normal file
105
src/State/RecurrenceHandler.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use App\Entity\Task;
|
||||
use App\Repository\TaskRepository;
|
||||
use App\Repository\TaskStatusRepository;
|
||||
use App\Service\CalDavService;
|
||||
use App\Service\RecurrenceCalculator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
final readonly class RecurrenceHandler
|
||||
{
|
||||
public function __construct(
|
||||
private RecurrenceCalculator $calculator,
|
||||
private TaskRepository $taskRepository,
|
||||
private TaskStatusRepository $statusRepository,
|
||||
private CalDavService $calDavService,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
public function handleIfNeeded(Task $task, bool $wasAlreadyFinal): void
|
||||
{
|
||||
// Only trigger on STATUS CHANGE to isFinal
|
||||
$currentStatus = $task->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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user