From df58b09c2e65187d56fda21c52d1ab65b0323979 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 19 Mar 2026 09:29:25 +0100 Subject: [PATCH] docs : add Zimbra CalDAV calendar integration design spec Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-19-zimbra-calendar-design.md | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-19-zimbra-calendar-design.md diff --git a/docs/superpowers/specs/2026-03-19-zimbra-calendar-design.md b/docs/superpowers/specs/2026-03-19-zimbra-calendar-design.md new file mode 100644 index 0000000..3e8c1b8 --- /dev/null +++ b/docs/superpowers/specs/2026-03-19-zimbra-calendar-design.md @@ -0,0 +1,232 @@ +# Intégration Calendrier Zimbra CalDAV + +**Date** : 2026-03-19 +**Statut** : Validé + +## Objectif + +Permettre de synchroniser les tâches Lesstime vers un calendrier Zimbra OVH via CalDAV. Sync one-way (push uniquement), avec support des tâches récurrentes. + +## Principes + +- **Push uniquement** : Lesstime pousse vers Zimbra, ne récupère jamais les événements existants +- **Opt-in** : les tâches ne sont pas envoyées au calendrier par défaut (checkbox décochée) +- **Sync synchrone** : les appels CalDAV se font au moment de l'action, timeout 5s +- **Configuration globale** : un seul compte Zimbra admin pour toute l'instance +- **Calendrier d'équipe** : toutes les tâches sync vont dans le même calendrier + +## Modèle de données + +### Nouveaux champs sur `Task` + +| Champ | Type | Nullable | Default | Description | +|---|---|---|---|---| +| `scheduledStart` | `DateTimeImmutable` | oui | `null` | Début du créneau planifié | +| `scheduledEnd` | `DateTimeImmutable` | oui | `null` | Fin du créneau planifié | +| `deadline` | `DateTimeImmutable` | oui | `null` | Date d'échéance | +| `syncToCalendar` | `bool` | non | `false` | Opt-in pour la sync Zimbra | +| `calendarEventUid` | `string` | oui | `null` | UID du VEVENT dans Zimbra | +| `calendarTodoUid` | `string` | oui | `null` | UID du VTODO dans Zimbra | + +### Nouvelle entité `TaskRecurrence` + +| Champ | Type | Nullable | Description | +|---|---|---|---| +| `id` | `int` | non | PK auto-increment | +| `type` | `string` | non | Enum : `daily`, `weekly`, `monthly`, `yearly` | +| `interval` | `int` | non | Tous les X (jours/semaines/mois/ans) | +| `daysOfWeek` | `json` | oui | Jours de la semaine pour hebdo, ex: `["monday","wednesday"]` | +| `dayOfMonth` | `int` | oui | Jour du mois pour mensuel, ex: `15` | +| `weekOfMonth` | `int` | oui | Semaine du mois, ex: `1` pour "le 1er X du mois" | +| `endDate` | `Date` | oui | Fin de la récurrence (null = infini) | +| `maxOccurrences` | `int` | oui | Nombre max d'occurrences (alternatif à endDate) | +| `occurrenceCount` | `int` | non | Compteur d'occurrences créées (default 0) | + +### Relations + +- `Task.recurrence` → `ManyToOne` vers `TaskRecurrence` (nullable) +- `TaskRecurrence.tasks` → `OneToMany` vers `Task` + +### Nouvelle entité `ZimbraConfiguration` + +| Champ | Type | Nullable | Description | +|---|---|---|---| +| `id` | `int` | non | PK auto-increment | +| `serverUrl` | `string` | non | URL CalDAV Zimbra | +| `username` | `string` | non | Compte Zimbra | +| `password` | `string` | non | Mot de passe (chiffré en BDD via sodium) | +| `calendarPath` | `string` | non | Chemin du calendrier, ex: `/Calendar/` | +| `enabled` | `bool` | non | Activer/désactiver la sync (default false) | + +## Service CalDAV + +### `CalDavService` + +Dépendances : `sabre/vobject` pour la génération ICS, requêtes HTTP via `Symfony\Contracts\HttpClient`. + +#### Méthodes + +- `createEvent(Task): string` — crée un VEVENT (créneau planifié), retourne l'UID +- `createTodo(Task): string` — crée un VTODO (deadline), retourne l'UID +- `updateEvent(Task): void` — met à jour le VEVENT existant +- `updateTodo(Task): void` — met à jour le VTODO existant +- `deleteEvent(string $uid): void` — supprime le VEVENT par UID +- `deleteTodo(string $uid): void` — supprime le VTODO par UID +- `testConnection(): bool` — teste la connexion CalDAV + +#### Format VEVENT (créneau planifié) + +``` +BEGIN:VEVENT +UID:{calendarEventUid} +SUMMARY:[PROJET-NUM] Titre de la tâche +DTSTART:{scheduledStart} +DTEND:{scheduledEnd} +DESCRIPTION:{description}\n\nLesstime: {url} +RRULE:{rrule si récurrence} +END:VEVENT +``` + +#### Format VTODO (deadline) + +``` +BEGIN:VTODO +UID:{calendarTodoUid} +SUMMARY:[PROJET-NUM] Titre de la tâche (deadline) +DUE:{deadline} +DESCRIPTION:{description}\n\nLesstime: {url} +END:VTODO +``` + +Pas de RRULE sur le VTODO — il suit la tâche courante uniquement. + +## Logique de sync + +### Déclenchement + +Un **Doctrine Entity Listener** sur `Task` (postPersist, postUpdate, preRemove) appelle le `CalDavService`. + +### Matrice d'actions + +| Action Lesstime | Effet CalDAV | +|---|---| +| Tâche créée/modifiée avec `syncToCalendar=true` et dates renseignées | Crée ou met à jour VEVENT + VTODO | +| `syncToCalendar` décoché | Supprime VEVENT + VTODO si existants | +| Tâche supprimée | Supprime VEVENT + VTODO si existants | +| Tâche récurrente passe en `isFinal` | Tâche archivée (`archived=true`), événements **conservés** dans Zimbra. Nouvelle tâche créée pointant vers le même VEVENT récurrent | +| Dates retirées | Supprime les events correspondants | + +### Gestion des erreurs + +- Timeout CalDAV : 5 secondes +- En cas d'échec : la tâche est quand même sauvegardée en BDD, un toast d'erreur est affiché côté frontend +- Les UIDs CalDAV restent `null` si la création a échoué + +## Tâches récurrentes + +### Comportement + +1. L'utilisateur crée une tâche avec récurrence dans Lesstime +2. **Zimbra** : un seul VEVENT avec `RRULE` est créé — Zimbra génère toutes les occurrences dans le calendrier automatiquement +3. **Lesstime** : une seule tâche existe à la fois +4. Quand la tâche passe en statut `isFinal` : + - La tâche est archivée automatiquement (`archived = true`) + - Les événements Zimbra sont **conservés** (historique) + - Une nouvelle tâche est créée avec : + - Même titre, description, assigné, tags, projet, groupe, effort, priorité + - Statut réinitialisé au premier statut (position la plus basse) + - Dates recalculées selon le pattern de récurrence (prochaine date selon le pattern, indépendamment de quand la tâche a été terminée) + - `calendarEventUid` pointant vers le même VEVENT récurrent + - Nouveau `calendarTodoUid` (nouvelle deadline) + - `occurrenceCount` incrémenté +5. Si `maxOccurrences` ou `endDate` atteint, la récurrence s'arrête (pas de nouvelle tâche créée) + +### Calcul de la prochaine date + +La prochaine date est calculée à partir de la date planifiée de la tâche courante (pas de la date de complétion) : + +- **Daily** : `scheduledStart + interval jours` +- **Weekly** : prochain jour de `daysOfWeek` à partir de `scheduledStart + interval semaines` +- **Monthly** : même `dayOfMonth` ou même `weekOfMonth`+jour, mois `+ interval` +- **Yearly** : même date, année `+ interval` + +La durée du créneau (`scheduledEnd - scheduledStart`) est conservée. + +## Frontend + +### Onglet "Planification" dans TaskModal + +La modale tâche existante aura 2 onglets : + +**Onglet "Détails"** (existant) : titre, description, statut, priorité, effort, assigné, tags, groupe + +**Onglet "Planification"** (nouveau) : + +#### Bloc Dates +- Date planifiée début (`datetime-local` picker) +- Date planifiée fin (`datetime-local` picker) +- Deadline (`date` picker) + +#### Bloc Calendrier +- Checkbox "Envoyer au calendrier" (décoché par défaut) +- Indicateur de statut sync (icône verte si sync OK, rouge si erreur, gris si non configuré) + +#### Bloc Récurrence +- Toggle "Tâche récurrente" +- Si activé : + - Type : Quotidien / Hebdomadaire / Mensuel / Annuel (select) + - Intervalle : "Tous les X ..." (input number) + - Conditionnel selon le type : + - Hebdomadaire → checkboxes jours de la semaine (Lu, Ma, Me, Je, Ve, Sa, Di) + - Mensuel → radio "Le X du mois" (input) ou "Le Xème [jour] du mois" (2 selects) + - Fin de récurrence : radio Jamais / Après X occurrences (input) / À une date (date picker) + +### Affichage des dates + +**Cartes Kanban (`TaskCard`)** : +- Badge deadline coloré : rouge si dépassée, orange si < 2 jours, gris sinon +- Icône calendrier si `syncToCalendar` activé +- Icône récurrence si tâche récurrente + +**Vue liste (`TaskListItem`)** : +- Colonne "Planifié" (date début) +- Colonne "Deadline" +- Icône récurrence si tâche récurrente + +**Page "Mes tâches"** : +- Même affichage que la vue liste +- Tri possible par deadline ou date planifiée + +### Page Admin — Configuration Zimbra + +Nouveau bloc dans la page admin existante : + +- URL du serveur CalDAV (input text) +- Nom d'utilisateur (input text) +- Mot de passe (input password) +- Chemin du calendrier (input text) +- Toggle activer/désactiver +- Bouton "Tester la connexion" (toast succès/erreur) + +Accessible uniquement `ROLE_ADMIN`. + +## MCP Tools + +### Mise à jour des tools existants + +`create-task` et `update-task` : nouveaux paramètres optionnels : +- `scheduledStart` (string datetime ISO) +- `scheduledEnd` (string datetime ISO) +- `deadline` (string datetime ISO) +- `syncToCalendar` (bool) + +### Nouveaux tools + +- `create-task-recurrence` — paramètres : taskId, type, interval, daysOfWeek?, dayOfMonth?, weekOfMonth?, endDate?, maxOccurrences? +- `update-task-recurrence` — paramètres : recurrenceId, + mêmes champs optionnels +- `delete-task-recurrence` — paramètres : recurrenceId — supprime la récurrence et l'événement récurrent Zimbra si existant + +## Dépendances PHP + +- `sabre/vobject` — génération/parsing ICS (VEVENT, VTODO, RRULE) +- `symfony/http-client` — requêtes HTTP CalDAV (PUT, DELETE, PROPFIND)