106 lines
3.6 KiB
PHP
106 lines
3.6 KiB
PHP
<?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?->getIsFinal() ?? 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();
|
|
}
|
|
}
|
|
}
|
|
}
|