# Édition d'une formation depuis le planning — Spécification de design

## Contexte

La vue planning affiche les formations en lecture seule. L'utilisateur veut pouvoir modifier une formation directement depuis cette vue, avec propagation immédiate vers Google Calendar. La suppression depuis cette vue est hors périmètre pour cette itération.

## Fonctionnalité

Bouton "Modifier" sur chaque ligne du tableau planning. Au clic, une modal s'ouvre avec un formulaire pré-rempli (mêmes champs que le formulaire de création). Soumission via AJAX, mise à jour de la ligne en place sans rechargement de page.

## Architecture

- **Authentification** : nouveau contrôleur PHP session-authentifié, distinct de l'API REST `X-API-Key` (pas de mélange des schémas d'auth)
- **Réutilisation** : `FormValidator` existant pour la validation, extraction de la logique de construction summary/description dans `GoogleCalendarService` pour la partager entre création et mise à jour
- **Frontend** : modal HTML cachée par défaut, ouverture/fermeture en JS pur, soumission via `fetch()`

## Endpoint serveur

### Nouveau contrôleur : `public/edit-event.php`

- Auth : `$auth->requireAuth()` (session)
- Méthode acceptée : `POST` (uniquement, ne pas confondre avec PUT de l'API REST)
- Query string : `?id=<eventId>` requis
- Body JSON : mêmes champs que le formulaire de création
  - `formation`, `client`, `lieu`, `statut` (requis)
  - `date_debut`, `date_fin` (requis, format `YYYY-MM-DD`)
  - `heure_debut`, `heure_fin` (requis, format `HH:MM`)
  - `description` (optionnel, notes libres sans les horaires)
- Validation : appel `FormValidator::validate()`
- Réponses :
  - `200` : `{id, summary, location, description, colorId, start, end, notes}` — l'événement mis à jour, plus le champ `notes` (description sans la première ligne d'horaires) pour faciliter le DOM update côté client
  - `400` : body JSON invalide ou paramètre `id` manquant
  - `404` : événement introuvable dans Google Calendar
  - `405` : méthode non autorisée
  - `422` : validation échouée — `{error, details: {champ: message}}`
  - `503` : Google Calendar non authentifié

### Extension `GoogleCalendarService`

Trois méthodes ajoutées :

- `static buildSummary(string $formation, string $client, string $lieu): string` — extrait depuis la concaténation actuelle dans `buildEventsData`
- `static buildDescription(string $heureDebut, string $heureFin, string $description): string` — extrait également
- `updateEventFromForm(string $eventId, array $data): array` — équivalent de `updateEvent` mais accepte les champs haut-niveau (formation, client, lieu, statut, dates, horaires, description) et reconstruit summary/description avant d'appeler l'API Google. Retourne le tableau de l'événement mis à jour (mêmes clés que `getEvent`) plus une clé `notes` (description sans la première ligne).

`buildEventsData` est refactorisé pour utiliser ces deux helpers (DRY).

## Frontend

### Modification du contrôleur `public/planning.php`

- Charger les référentiels (`formations`, `clients`, `lieux`) via `$referentials->getAll()` pour pré-remplir les Tom Select de la modal
- Pour chaque événement, ajouter dans le tableau `$events` une clé `notes` = description sans la première ligne d'horaires (déjà calculée dans le foreach existant)

### Modification du template `templates/planning.php`

**Nouvelle première colonne** : "Actions" avec un bouton "Modifier" par ligne, portant un `data-id` (l'eventId Google).

**Attributs supplémentaires sur chaque `<tr>`** :
- `data-id` : eventId
- `data-formation`, `data-client`, `data-lieu` (déjà présents)
- `data-statut` (déjà présent)
- `data-end` : date de fin inclusive (en plus de `data-start`)
- `data-heure-debut`, `data-heure-fin` : extraits des horaires
- `data-notes` : description sans la première ligne

**Modal HTML** : ajoutée à la fin du template, structure :

```html
<div id="edit-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 ...">
  <div class="bg-white rounded-lg shadow-xl max-w-2xl mx-auto mt-12 p-6">
    <div id="edit-error" class="hidden bg-red-100 ..."></div>
    <form id="edit-form" class="space-y-4">
      <!-- mêmes champs que templates/form.php -->
    </form>
    <div class="flex justify-end gap-2 mt-4">
      <button type="button" id="edit-cancel">Annuler</button>
      <button type="submit" form="edit-form" id="edit-submit">Enregistrer</button>
    </div>
  </div>
</div>
```

**Listes pré-remplies** : les `<option>` des selects formation/client/lieu sont rendues côté serveur depuis les référentiels chargés par le contrôleur.

**Tom Select** : appliqué à formation/client/lieu (cohérent avec le formulaire de création).

### JavaScript ajouté

Module IIFE en bas du template, après le code de filtrage/tri existant :

**Ouverture** :
- Délégation de clic sur les boutons `.edit-btn` du tableau
- Lecture des `data-*` de la ligne
- Pré-remplissage des champs du formulaire :
  - Pour formation/client/lieu : si la valeur n'existe pas dans les options du Tom Select, l'injecter dynamiquement via `tomSelect.addOption({value: x, text: x})` puis `tomSelect.setValue(x)`
  - Pour les autres : assignation directe
- Mise à jour de l'attribut `data-event-id` sur la modal pour le retrouver à la soumission
- Affichage de la modal (retirer `hidden`), focus sur le premier champ
- Effacement des messages d'erreur précédents

**Fermeture** :
- Clic sur le bouton "Annuler"
- Clic sur le bouton ✕ (en haut à droite de la modal)
- Clic sur le backdrop (en dehors du contenu de la modal)
- Touche Échap

**Soumission** :
- `event.preventDefault()`
- Désactivation du bouton "Enregistrer", texte → "Enregistrement…"
- Construction du body JSON depuis les champs du form
- `fetch('/edit-event.php?id=' + eventId, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) })`
- Selon `response.status` :
  - `200` : appeler `updateRow(rowElement, json)` puis fermer la modal
  - `422` : afficher `json.details` sous chaque champ
  - autre : afficher `json.error` dans le bandeau d'erreur de la modal
- Réactivation du bouton "Enregistrer"

**Mise à jour de la ligne en place** (`updateRow(row, event)`) :
- Met à jour les `data-*` (statut, formation, client, lieu, start, end, heure-debut, heure-fin, notes)
- Met à jour le contenu textuel des cellules (statut + pastille, formation, client, lieu, dates formatées, horaires)
- Helper JS `formatDates(start, end)` pour reproduire le formatage français du contrôleur PHP ("15 avr.", "15 → 17 avr.", "15 avr. → 2 mai")
- Re-applique les filtres et le striping pour préserver la cohérence visuelle

## Gestion des erreurs

| Cas | Statut HTTP | Comportement UI |
|-----|-------------|-----------------|
| Validation échouée | 422 | Messages d'erreur inline sous chaque champ concerné |
| Événement supprimé entre-temps | 404 | Bandeau rouge en haut de modal |
| Token Google expiré | 503 | Bandeau rouge invitant à se reconnecter |
| Erreur réseau / 500 | — | Bandeau rouge avec message générique |
| Body JSON invalide | 400 | Bandeau rouge |

## Tests

- Tests unitaires pour `GoogleCalendarService::buildSummary` et `buildDescription` (logique de chaîne, facile à tester, réutilisée par création + édition)
- Pas de tests pour `updateEventFromForm` ni le contrôleur (intégration Google Calendar, cohérent avec le reste du projet)

## Fichiers impactés

| Fichier | Action |
|---------|--------|
| `src/GoogleCalendarService.php` | Modifier — ajouter `buildSummary`, `buildDescription`, `updateEventFromForm`, refactor `buildEventsData` |
| `public/edit-event.php` | Créer — contrôleur session-auth |
| `public/planning.php` | Modifier — charger référentiels, calculer `notes` |
| `templates/planning.php` | Modifier — colonne Modifier, modal, JS d'édition, attributs supplémentaires sur les `<tr>` |
| `tests/GoogleCalendarServiceTest.php` | Créer ou modifier — tests pour `buildSummary` et `buildDescription` |

## Hors périmètre

- Pas de suppression depuis le planning (planifié pour une itération ultérieure)
- Pas de modification de l'API REST `/api/calendar.php` (reste pour les agents externes)
- Pas de fallback sans JavaScript (la page planning en a déjà besoin pour filtres/tri)
- Pas d'historique des modifications
