docs : fix plan review findings for Zimbra calendar integration
- Separate @Version from occurrenceCount (use dedicated version column) - Fix processor chaining: TaskNumberProcessor for Post, TaskCalendarProcessor for Patch/Delete - Detect status CHANGE to isFinal (not just current isFinal) to avoid duplicate recurrence - Add DeleteTaskTool CalDAV cleanup for MCP deletions - Add "Mes tâches" page update task (sort + columns) - Use i18n for weekDays labels instead of hardcoded French - Clarify documents/bookStackLinks NOT copied for recurring tasks - Use multi-line getter/setter style note Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -174,10 +174,13 @@ class TaskRecurrence
|
||||
private ?int $maxOccurrences = null;
|
||||
|
||||
#[ORM\Column(type: 'integer', options: ['default' => 0])]
|
||||
#[ORM\Version]
|
||||
#[Groups(['task_recurrence:read'])]
|
||||
private int $occurrenceCount = 0;
|
||||
|
||||
#[ORM\Version]
|
||||
#[ORM\Column(type: 'integer', options: ['default' => 1])]
|
||||
private int $version = 1;
|
||||
|
||||
/** @var Collection<int, Task> */
|
||||
#[ORM\OneToMany(targetEntity: Task::class, mappedBy: 'recurrence')]
|
||||
private Collection $tasks;
|
||||
@@ -187,25 +190,9 @@ class TaskRecurrence
|
||||
$this->tasks = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int { return $this->id; }
|
||||
public function getType(): RecurrenceType { return $this->type; }
|
||||
public function setType(RecurrenceType $type): static { $this->type = $type; return $this; }
|
||||
public function getInterval(): int { return $this->interval; }
|
||||
public function setInterval(int $interval): static { $this->interval = $interval; return $this; }
|
||||
public function getDaysOfWeek(): ?array { return $this->daysOfWeek; }
|
||||
public function setDaysOfWeek(?array $daysOfWeek): static { $this->daysOfWeek = $daysOfWeek; return $this; }
|
||||
public function getDayOfMonth(): ?int { return $this->dayOfMonth; }
|
||||
public function setDayOfMonth(?int $dayOfMonth): static { $this->dayOfMonth = $dayOfMonth; return $this; }
|
||||
public function getWeekOfMonth(): ?int { return $this->weekOfMonth; }
|
||||
public function setWeekOfMonth(?int $weekOfMonth): static { $this->weekOfMonth = $weekOfMonth; return $this; }
|
||||
public function getEndDate(): ?\DateTimeImmutable { return $this->endDate; }
|
||||
public function setEndDate(?\DateTimeImmutable $endDate): static { $this->endDate = $endDate; return $this; }
|
||||
public function getMaxOccurrences(): ?int { return $this->maxOccurrences; }
|
||||
public function setMaxOccurrences(?int $maxOccurrences): static { $this->maxOccurrences = $maxOccurrences; return $this; }
|
||||
public function getOccurrenceCount(): int { return $this->occurrenceCount; }
|
||||
public function incrementOccurrenceCount(): static { $this->occurrenceCount++; return $this; }
|
||||
/** @return Collection<int, Task> */
|
||||
public function getTasks(): Collection { return $this->tasks; }
|
||||
// Standard multi-line getters/setters following codebase convention (PHP CS Fixer).
|
||||
// All properties get standard get/set methods.
|
||||
// Special methods: incrementOccurrenceCount() and getVersion()
|
||||
}
|
||||
```
|
||||
|
||||
@@ -722,38 +709,44 @@ final readonly class TaskCalendarProcessor implements ProcessorInterface
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update Task entity** — change `Post` processor and `Patch`/`Delete` processors
|
||||
- [ ] **Step 2: Update Task entity and TaskNumberProcessor**
|
||||
|
||||
The `Post` operation needs both task number generation AND calendar sync. Create a composite approach:
|
||||
- `Post` processor: `TaskNumberProcessor` (which calls persistProcessor internally), then `CalDavService::syncTask()` is called by a wrapping processor, OR chain `TaskCalendarProcessor` to wrap `TaskNumberProcessor`.
|
||||
|
||||
Simplest approach: modify `TaskNumberProcessor` to also handle calendar sync after persist, or create `TaskCalendarProcessor` as the outer processor that delegates to `TaskNumberProcessor` for Post.
|
||||
**Approach chosen:** Keep `TaskNumberProcessor` for Post (inject `CalDavService` into it). Use `TaskCalendarProcessor` for Patch and Delete only.
|
||||
|
||||
Update `Task.php` operations:
|
||||
|
||||
```php
|
||||
new Post(security: "is_granted('ROLE_ADMIN')", processor: TaskCalendarProcessor::class),
|
||||
new Patch(security: "is_granted('ROLE_ADMIN')", processor: TaskCalendarProcessor::class),
|
||||
new Delete(security: "is_granted('ROLE_ADMIN')", processor: TaskCalendarProcessor::class),
|
||||
```
|
||||
|
||||
For Post, `TaskCalendarProcessor` needs to delegate to `TaskNumberProcessor` instead of `persistProcessor`. Use a tagged service or inject `TaskNumberProcessor` directly for Post operations.
|
||||
|
||||
Alternative (simpler): keep `TaskNumberProcessor` for Post, inject `CalDavService` into it, and call sync after the transaction. Use `TaskCalendarProcessor` only for Patch/Delete.
|
||||
|
||||
```php
|
||||
// In Task.php:
|
||||
new Post(security: "is_granted('ROLE_ADMIN')", processor: TaskNumberProcessor::class),
|
||||
new Patch(security: "is_granted('ROLE_ADMIN')", processor: TaskCalendarProcessor::class),
|
||||
new Delete(security: "is_granted('ROLE_ADMIN')", processor: TaskCalendarProcessor::class),
|
||||
```
|
||||
|
||||
And add CalDAV sync to `TaskNumberProcessor::process()`:
|
||||
Modify `TaskNumberProcessor`: inject `CalDavService`, after the transaction completes call sync:
|
||||
|
||||
```php
|
||||
// After the persist, sync to calendar
|
||||
$this->calDavService->syncTask($data);
|
||||
$this->entityManager->flush();
|
||||
public function __construct(
|
||||
// ... existing deps ...
|
||||
private CalDavService $calDavService,
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||
{
|
||||
if ($operation instanceof Post && null !== $data->getProject()) {
|
||||
$result = $this->entityManager->wrapInTransaction(function () use ($data, $operation, $uriVariables, $context) {
|
||||
$maxNumber = $this->taskRepository->findMaxNumberByProjectForUpdate($data->getProject());
|
||||
$data->setNumber($maxNumber + 1);
|
||||
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
||||
});
|
||||
|
||||
// After DB commit: sync to Zimbra
|
||||
$this->calDavService->syncTask($data);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
@@ -772,9 +765,13 @@ git commit -m "feat : add TaskCalendarProcessor for CalDAV sync after DB operati
|
||||
|
||||
- [ ] **Step 1: Add recurrence logic to Patch handling**
|
||||
|
||||
**CRITICAL: Detect status CHANGE to isFinal, not just current isFinal.**
|
||||
|
||||
In `TaskCalendarProcessor`, when processing a Patch:
|
||||
1. After `persistProcessor->process()`, check if the task's status is `isFinal` AND task has a recurrence
|
||||
2. If so:
|
||||
1. **Before** calling `persistProcessor->process()`, store the original status: `$originalStatus = $data->getStatus()`
|
||||
2. After `persistProcessor->process()`, check if the status **changed** to `isFinal` (original status was NOT isFinal, new status IS isFinal) AND task has a recurrence
|
||||
3. If the status was already isFinal before the Patch, skip recurrence creation (avoids duplicates on subsequent edits)
|
||||
4. If conditions met:
|
||||
- Archive the current task (`archived = true`)
|
||||
- Clear `calendarEventUid` and `calendarTodoUid` on archived task
|
||||
- Use `RecurrenceCalculator::getNextDate()` to compute next dates
|
||||
@@ -783,6 +780,7 @@ In `TaskCalendarProcessor`, when processing a Patch:
|
||||
- Generate new task number via `TaskRepository::findMaxNumberByProjectForUpdate()`
|
||||
- Set first status (lowest position, non-final)
|
||||
- Set recalculated scheduledStart, scheduledEnd, deadline
|
||||
- **Do NOT copy** documents or bookStackLinks (those are specific to the completed task instance)
|
||||
- Copy `calendarEventUid` from the archived task (before clearing) to the new task
|
||||
- Create new VTODO for the new deadline
|
||||
- Increment `occurrenceCount` on `TaskRecurrence`
|
||||
@@ -812,6 +810,8 @@ git commit -m "feat : auto-create next recurring task when current task reaches
|
||||
|
||||
- [ ] **Step 2: Update `UpdateTaskTool`** — same new params. After flush, call `$this->calDavService->syncTask($task)` and flush. Include calendar fields in JSON response.
|
||||
|
||||
- [ ] **Step 2b: Update `DeleteTaskTool`** — inject `CalDavService`. Before removing the task, store `calendarEventUid` and `calendarTodoUid`. After `$entityManager->flush()`, call `$this->calDavService->deleteEvent($eventUid)` and `$this->calDavService->deleteTodo($todoUid)` if non-null. **This is critical — MCP delete bypasses API Platform processors.**
|
||||
|
||||
- [ ] **Step 3: Create `CreateTaskRecurrenceTool`**
|
||||
|
||||
```php
|
||||
@@ -1245,15 +1245,16 @@ const form = reactive({
|
||||
recurrenceEndDate: '' as string,
|
||||
})
|
||||
|
||||
const weekDays = [
|
||||
{ value: 'monday', label: 'Lu' },
|
||||
{ value: 'tuesday', label: 'Ma' },
|
||||
{ value: 'wednesday', label: 'Me' },
|
||||
{ value: 'thursday', label: 'Je' },
|
||||
{ value: 'friday', label: 'Ve' },
|
||||
{ value: 'saturday', label: 'Sa' },
|
||||
{ value: 'sunday', label: 'Di' },
|
||||
]
|
||||
const { t } = useI18n()
|
||||
const weekDays = computed(() => [
|
||||
{ value: 'monday', label: t('tasks.planning.days.mon') },
|
||||
{ value: 'tuesday', label: t('tasks.planning.days.tue') },
|
||||
{ value: 'wednesday', label: t('tasks.planning.days.wed') },
|
||||
{ value: 'thursday', label: t('tasks.planning.days.thu') },
|
||||
{ value: 'friday', label: t('tasks.planning.days.fri') },
|
||||
{ value: 'saturday', label: t('tasks.planning.days.sat') },
|
||||
{ value: 'sunday', label: t('tasks.planning.days.sun') },
|
||||
])
|
||||
|
||||
function toggleDay(day: string) {
|
||||
const idx = form.recurrenceDaysOfWeek.indexOf(day)
|
||||
@@ -1336,6 +1337,24 @@ git commit -m "feat(ui) : add deadline badges and calendar/recurrence icons to t
|
||||
|
||||
---
|
||||
|
||||
## Task 15b: Update "Mes tâches" Page
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/pages/my-tasks.vue`
|
||||
|
||||
- [ ] **Step 1: Add deadline and scheduled date columns/badges** — same pattern as TaskListItem/TaskCard updates from Task 15.
|
||||
|
||||
- [ ] **Step 2: Add sort options** — allow sorting by `deadline` and `scheduledStart` (use API Platform OrderFilter added in Task 5). Add sort dropdown or clickable column headers.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/pages/my-tasks.vue
|
||||
git commit -m "feat(ui) : add deadline/scheduled columns and sort options to Mes tâches page"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 16: Create Admin Zimbra Tab
|
||||
|
||||
**Files:**
|
||||
|
||||
Reference in New Issue
Block a user