# Planning Edit Modal — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add the ability to edit a formation directly from the planning view via a modal, with changes propagated to Google Calendar.

**Architecture:** New session-authenticated PHP controller (`edit-event.php`) accepting JSON, validating via `FormValidator`, and calling a new `GoogleCalendarService::updateEventFromForm()` method. Frontend modal with vanilla JS, AJAX submission, in-place row update.

**Tech Stack:** PHP 8.x, Tailwind CSS (CDN), Tom Select (CDN), vanilla JavaScript, PHPUnit

---

## File Structure

| File | Action | Responsibility |
|------|--------|---------------|
| `src/GoogleCalendarService.php` | Modify | Add `buildSummary`, `buildDescription`, `updateEventFromForm`. Refactor `buildEventsData` to use helpers. |
| `tests/GoogleCalendarServiceTest.php` | Modify | Add tests for `buildSummary` and `buildDescription` |
| `public/planning.php` | Modify | Load referentials, compute `notes`/`endDate`/`heureDebut`/`heureFin` for each event |
| `templates/planning.php` | Modify | Add Actions column, data-* attributes, modal HTML, edit JS |
| `public/edit-event.php` | Create | Session-authenticated controller for AJAX edit |

---

### Task 1: Extract `buildSummary` and `buildDescription` helpers (TDD)

**Files:**
- Modify: `src/GoogleCalendarService.php`
- Test: `tests/GoogleCalendarServiceTest.php`

- [ ] **Step 1: Write failing tests for `buildSummary` and `buildDescription`**

Add to the end of `tests/GoogleCalendarServiceTest.php`, just before the final `}`:

```php
    public function testBuildSummaryConcatenatesFormationClientLieu(): void
    {
        $this->assertSame(
            'PHP Avancé - Acme Corp - Lyon',
            GoogleCalendarService::buildSummary('PHP Avancé', 'Acme Corp', 'Lyon')
        );
    }

    public function testBuildDescriptionWithoutNotes(): void
    {
        $this->assertSame(
            '09:00 - 17:00',
            GoogleCalendarService::buildDescription('09:00', '17:00', '')
        );
    }

    public function testBuildDescriptionWithNotes(): void
    {
        $this->assertSame(
            "09:00 - 17:00\nFormation intra",
            GoogleCalendarService::buildDescription('09:00', '17:00', 'Formation intra')
        );
    }
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `vendor/bin/phpunit tests/GoogleCalendarServiceTest.php`
Expected: 3 errors with "Method buildSummary not found" / "Method buildDescription not found"

- [ ] **Step 3: Add the two static helpers**

In `src/GoogleCalendarService.php`, add before the final `}`:

```php
    public static function buildSummary(string $formation, string $client, string $lieu): string
    {
        return $formation . ' - ' . $client . ' - ' . $lieu;
    }

    public static function buildDescription(string $timeStart, string $timeEnd, string $description): string
    {
        $full = $timeStart . ' - ' . $timeEnd;
        if ($description !== '') {
            $full .= "\n" . $description;
        }
        return $full;
    }
```

- [ ] **Step 4: Refactor `buildEventsData` to use the new helper**

Replace the body of `buildEventsData` in `src/GoogleCalendarService.php` (currently lines 200-222) with:

```php
    public static function buildEventsData(string $title, string $dateStart, string $dateEnd, string $timeStart, string $timeEnd, string $location, string $description, int $colorId): array
    {
        $fullDescription = self::buildDescription($timeStart, $timeEnd, $description);

        // Google Calendar: end date is exclusive, so +1 day
        $endExclusive = (new \DateTimeImmutable($dateEnd))->modify('+1 day')->format('Y-m-d');

        return [[
            'summary' => $title,
            'location' => $location,
            'description' => $fullDescription,
            'colorId' => $colorId,
            'start' => [
                'date' => $dateStart,
            ],
            'end' => [
                'date' => $endExclusive,
            ],
        ]];
    }
```

- [ ] **Step 5: Run all tests to verify everything still passes**

Run: `vendor/bin/phpunit`
Expected: All tests PASS (existing 8 + new 3 = 11 in GoogleCalendarServiceTest)

- [ ] **Step 6: Commit**

```bash
git add src/GoogleCalendarService.php tests/GoogleCalendarServiceTest.php
git commit -m "refactor: extract buildSummary and buildDescription helpers"
```

---

### Task 2: Add `updateEventFromForm` method

**Files:**
- Modify: `src/GoogleCalendarService.php`

- [ ] **Step 1: Add the `updateEventFromForm` method**

In `src/GoogleCalendarService.php`, add after the existing `updateEvent` method (after line 187, before `deleteEvent`):

```php
    public function updateEventFromForm(string $eventId, array $data): array
    {
        $calendar = new GoogleCalendar($this->client);
        $event = $calendar->events->get($this->calendarId, $eventId);

        $event->setSummary(self::buildSummary($data['formation'], $data['client'], $data['lieu']));
        $event->setLocation($data['lieu']);
        $event->setDescription(self::buildDescription($data['heure_debut'], $data['heure_fin'], $data['description'] ?? ''));
        $event->setColorId((string) self::getColorId($data['statut']));

        $start = new \Google\Service\Calendar\EventDateTime();
        $start->setDate($data['date_debut']);
        $event->setStart($start);

        $endExclusive = (new \DateTimeImmutable($data['date_fin']))->modify('+1 day')->format('Y-m-d');
        $end = new \Google\Service\Calendar\EventDateTime();
        $end->setDate($endExclusive);
        $event->setEnd($end);

        $updated = $calendar->events->update($this->calendarId, $eventId, $event);
        $startDt = $updated->getStart();
        $endDt = $updated->getEnd();

        $endDate = $endDt->getDate();
        $endInclusive = $endDate ? (new \DateTimeImmutable($endDate))->modify('-1 day')->format('Y-m-d') : null;

        $description = (string) $updated->getDescription();
        $firstLine = explode("\n", $description, 2)[0];
        $notes = '';
        if (preg_match('/^\d{2}:\d{2}\s*-\s*\d{2}:\d{2}$/', trim($firstLine))) {
            $parts = explode("\n", $description, 2);
            $notes = $parts[1] ?? '';
        }

        return [
            'id' => $updated->getId(),
            'summary' => $updated->getSummary(),
            'location' => $updated->getLocation(),
            'description' => $description,
            'colorId' => $updated->getColorId(),
            'start' => $startDt->getDate() ?: $startDt->getDateTime(),
            'end' => $endDt->getDate() ?: $endDt->getDateTime(),
            'endInclusive' => $endInclusive,
            'notes' => $notes,
            'heure_debut' => $data['heure_debut'],
            'heure_fin' => $data['heure_fin'],
        ];
    }
```

- [ ] **Step 2: Verify syntax**

Run: `php -l src/GoogleCalendarService.php`
Expected: `No syntax errors detected`

- [ ] **Step 3: Verify all tests still pass**

Run: `vendor/bin/phpunit`
Expected: All tests PASS

- [ ] **Step 4: Commit**

```bash
git add src/GoogleCalendarService.php
git commit -m "feat: add updateEventFromForm method to GoogleCalendarService"
```

---

### Task 3: Create the `edit-event.php` controller

**Files:**
- Create: `public/edit-event.php`

- [ ] **Step 1: Create the controller**

Create `public/edit-event.php`:

```php
<?php

require_once __DIR__ . '/bootstrap.php';

use Fgaurat\EolemPlanning\FormValidator;

header('Content-Type: application/json; charset=utf-8');

$auth->requireAuth();

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['error' => 'Méthode non autorisée']);
    exit;
}

$eventId = $_GET['id'] ?? '';
if ($eventId === '') {
    http_response_code(400);
    echo json_encode(['error' => 'Paramètre id requis']);
    exit;
}

$data = json_decode(file_get_contents('php://input'), true);
if (!is_array($data)) {
    http_response_code(400);
    echo json_encode(['error' => 'Body JSON invalide']);
    exit;
}

$data['heure_debut'] = $data['heure_debut'] ?? '09:00';
$data['heure_fin'] = $data['heure_fin'] ?? '17:00';
$data['description'] = $data['description'] ?? '';

$validator = new FormValidator();
$errors = $validator->validate($data);

if (!empty($errors)) {
    http_response_code(422);
    echo json_encode(['error' => 'Validation échouée', 'details' => $errors], JSON_UNESCAPED_UNICODE);
    exit;
}

$gcal = getGoogleCalendarService();

if (!$gcal->isAuthenticated()) {
    http_response_code(503);
    echo json_encode(['error' => 'Google Calendar non authentifié. Veuillez vous reconnecter via le formulaire de création.']);
    exit;
}

try {
    $updated = $gcal->updateEventFromForm($eventId, $data);
    echo json_encode($updated, JSON_UNESCAPED_UNICODE);
} catch (\Google\Service\Exception $e) {
    $code = $e->getCode() ?: 500;
    if ($code === 404) {
        http_response_code(404);
        echo json_encode(['error' => 'Événement introuvable dans Google Calendar']);
    } else {
        http_response_code($code);
        error_log('edit-event Google API error: ' . $e->getMessage());
        echo json_encode(['error' => 'Erreur Google Calendar', 'details' => $e->getMessage()]);
    }
} catch (\Throwable $e) {
    http_response_code(500);
    error_log('edit-event error: ' . $e->getMessage());
    echo json_encode(['error' => 'Erreur interne', 'details' => $e->getMessage()]);
}
```

- [ ] **Step 2: Verify syntax**

Run: `php -l public/edit-event.php`
Expected: `No syntax errors detected`

- [ ] **Step 3: Commit**

```bash
git add public/edit-event.php
git commit -m "feat: add session-authenticated edit-event controller"
```

---

### Task 4: Extend `public/planning.php` to expose edit data

**Files:**
- Modify: `public/planning.php`

- [ ] **Step 1: Add referentials and extra event fields**

Locate in `public/planning.php` the line:
```php
$gcal = getGoogleCalendarService();
```

Replace it with:
```php
$gcal = getGoogleCalendarService();

$refFormations = $referentials->getAll('formations');
$refClients = $referentials->getAll('clients');
$refLieux = $referentials->getAll('lieux');
```

- [ ] **Step 2: Compute extra event fields**

In the `foreach ($rawEvents as $ev)` loop in `public/planning.php`, locate the block:

```php
        // Hours from first line of description
        $desc = $ev['description'] ?? '';
        $firstLine = explode("\n", $desc, 2)[0];
        $horaires = (preg_match('/^\d{2}:\d{2}\s*-\s*\d{2}:\d{2}$/', trim($firstLine))) ? trim($firstLine) : '';
```

Replace it with:

```php
        // Hours from first line of description, notes from rest
        $desc = $ev['description'] ?? '';
        $firstLine = explode("\n", $desc, 2)[0];
        $horaires = '';
        $heureDebut = '';
        $heureFin = '';
        $notes = '';
        if (preg_match('/^(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})$/', trim($firstLine), $m)) {
            $horaires = trim($firstLine);
            $heureDebut = $m[1];
            $heureFin = $m[2];
            $parts = explode("\n", $desc, 2);
            $notes = $parts[1] ?? '';
        } else {
            $notes = $desc;
        }
```

- [ ] **Step 3: Add new fields to the `$events[]` array**

In the same loop, locate the `$events[] = [...]` block and replace it with:

```php
        $events[] = [
            'id'           => $ev['id'] ?? '',
            'formation'    => $formation,
            'client'       => $client,
            'lieu'         => $lieu,
            'statutLabel'  => $statutLabel,
            'statutColor'  => $statutColor,
            'startDate'    => $startDate,
            'endDate'      => $endDate,
            'datesDisplay' => $datesDisplay,
            'horaires'     => $horaires,
            'heureDebut'   => $heureDebut,
            'heureFin'     => $heureFin,
            'notes'        => $notes,
        ];
```

- [ ] **Step 4: Verify syntax**

Run: `php -l public/planning.php`
Expected: `No syntax errors detected`

- [ ] **Step 5: Commit**

```bash
git add public/planning.php
git commit -m "feat: expose referentials and edit fields in planning controller"
```

---

### Task 5: Add Actions column and data-* attributes to planning template

**Files:**
- Modify: `templates/planning.php`

- [ ] **Step 1: Add the Actions header column**

In `templates/planning.php`, locate the `<thead>` block:

```php
        <thead class="bg-gray-50">
            <tr>
                <th class="text-left py-2 px-4 font-medium text-gray-600 text-sm cursor-pointer hover:text-gray-900 select-none" data-sort="statut">Statut <span class="sort-arrow text-xs"></span></th>
```

Insert a new `<th>` as the first column inside the `<tr>`:

```php
        <thead class="bg-gray-50">
            <tr>
                <th class="text-left py-2 px-4 font-medium text-gray-600 text-sm">Actions</th>
                <th class="text-left py-2 px-4 font-medium text-gray-600 text-sm cursor-pointer hover:text-gray-900 select-none" data-sort="statut">Statut <span class="sort-arrow text-xs"></span></th>
```

- [ ] **Step 2: Add data-* attributes and Actions cell on each row**

Locate the `<tr>` and `<td>` cells inside the `foreach ($events as $i => $ev)` loop. Replace the entire block:

```php
            <?php foreach ($events as $i => $ev): ?>
            <tr class="border-t border-gray-100"
                data-statut="<?= htmlspecialchars($ev['statutLabel']) ?>"
                data-client="<?= htmlspecialchars($ev['client']) ?>"
                data-formation="<?= htmlspecialchars($ev['formation']) ?>"
                data-lieu="<?= htmlspecialchars($ev['lieu']) ?>"
                data-start="<?= htmlspecialchars($ev['startDate']) ?>">
                <td class="py-2 px-4 text-sm whitespace-nowrap">
                    <?php if ($ev['statutColor'] !== ''): ?>
                        <span class="inline-block w-3 h-3 rounded-full mr-1 align-middle" style="background-color: <?= htmlspecialchars($ev['statutColor']) ?>"></span>
                    <?php endif; ?>
                    <?= htmlspecialchars($ev['statutLabel']) ?>
                </td>
                <td class="py-2 px-4 text-sm"><?= htmlspecialchars($ev['formation']) ?></td>
                <td class="py-2 px-4 text-sm"><?= htmlspecialchars($ev['client']) ?></td>
                <td class="py-2 px-4 text-sm"><?= htmlspecialchars($ev['lieu']) ?></td>
                <td class="py-2 px-4 text-sm whitespace-nowrap"><?= htmlspecialchars($ev['datesDisplay']) ?></td>
                <td class="py-2 px-4 text-sm whitespace-nowrap"><?= htmlspecialchars($ev['horaires']) ?></td>
            </tr>
            <?php endforeach; ?>
```

with:

```php
            <?php foreach ($events as $i => $ev): ?>
            <tr class="border-t border-gray-100"
                data-id="<?= htmlspecialchars($ev['id']) ?>"
                data-statut="<?= htmlspecialchars($ev['statutLabel']) ?>"
                data-client="<?= htmlspecialchars($ev['client']) ?>"
                data-formation="<?= htmlspecialchars($ev['formation']) ?>"
                data-lieu="<?= htmlspecialchars($ev['lieu']) ?>"
                data-start="<?= htmlspecialchars($ev['startDate']) ?>"
                data-end="<?= htmlspecialchars($ev['endDate']) ?>"
                data-heure-debut="<?= htmlspecialchars($ev['heureDebut']) ?>"
                data-heure-fin="<?= htmlspecialchars($ev['heureFin']) ?>"
                data-notes="<?= htmlspecialchars($ev['notes']) ?>"
                data-statut-key="<?= htmlspecialchars(strtolower(str_replace(['é', 'è'], ['e', 'e'], $ev['statutLabel']))) ?>">
                <td class="py-2 px-4 text-sm whitespace-nowrap">
                    <button type="button" class="edit-btn text-blue-600 hover:underline text-sm">Modifier</button>
                </td>
                <td class="py-2 px-4 text-sm whitespace-nowrap">
                    <?php if ($ev['statutColor'] !== ''): ?>
                        <span class="status-dot inline-block w-3 h-3 rounded-full mr-1 align-middle" style="background-color: <?= htmlspecialchars($ev['statutColor']) ?>"></span>
                    <?php endif; ?>
                    <span class="status-label"><?= htmlspecialchars($ev['statutLabel']) ?></span>
                </td>
                <td class="cell-formation py-2 px-4 text-sm"><?= htmlspecialchars($ev['formation']) ?></td>
                <td class="cell-client py-2 px-4 text-sm"><?= htmlspecialchars($ev['client']) ?></td>
                <td class="cell-lieu py-2 px-4 text-sm"><?= htmlspecialchars($ev['lieu']) ?></td>
                <td class="cell-dates py-2 px-4 text-sm whitespace-nowrap"><?= htmlspecialchars($ev['datesDisplay']) ?></td>
                <td class="cell-horaires py-2 px-4 text-sm whitespace-nowrap"><?= htmlspecialchars($ev['horaires']) ?></td>
            </tr>
            <?php endforeach; ?>
```

- [ ] **Step 3: Verify syntax**

Run: `php -l templates/planning.php`
Expected: `No syntax errors detected`

- [ ] **Step 4: Commit**

```bash
git add templates/planning.php
git commit -m "feat: add Actions column and data-* attributes to planning rows"
```

---

### Task 6: Add the edit modal and JavaScript

**Files:**
- Modify: `templates/planning.php`

- [ ] **Step 1: Add the modal HTML and JS at the end of the template**

Locate the closing `<?php endif; ?>` at the very end of `templates/planning.php`. Insert the following block **before** that closing `<?php endif; ?>`:

```php
<!-- Edit modal -->
<div id="edit-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 overflow-y-auto" data-event-id="">
    <div class="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-auto mt-12 mb-12 p-6">
        <div class="flex items-center justify-between mb-4">
            <h2 class="text-xl font-bold text-gray-800">Modifier la formation</h2>
            <button type="button" id="edit-close" class="text-gray-400 hover:text-gray-700 text-2xl leading-none">&times;</button>
        </div>

        <div id="edit-error" class="hidden bg-red-100 text-red-700 p-3 rounded mb-4 text-sm"></div>

        <form id="edit-form" class="space-y-4">
            <div>
                <label for="edit-formation" class="block text-sm font-medium text-gray-700 mb-1">Formation</label>
                <select id="edit-formation" name="formation" required>
                    <option value="">Sélectionner...</option>
                    <?php foreach ($refFormations as $f): ?>
                        <option value="<?= htmlspecialchars($f) ?>"><?= htmlspecialchars($f) ?></option>
                    <?php endforeach; ?>
                </select>
                <div class="field-error text-red-600 text-xs mt-1 hidden"></div>
            </div>

            <div>
                <label for="edit-client" class="block text-sm font-medium text-gray-700 mb-1">Client</label>
                <select id="edit-client" name="client" required>
                    <option value="">Sélectionner...</option>
                    <?php foreach ($refClients as $c): ?>
                        <option value="<?= htmlspecialchars($c) ?>"><?= htmlspecialchars($c) ?></option>
                    <?php endforeach; ?>
                </select>
                <div class="field-error text-red-600 text-xs mt-1 hidden"></div>
            </div>

            <div>
                <label for="edit-lieu" class="block text-sm font-medium text-gray-700 mb-1">Lieu</label>
                <select id="edit-lieu" name="lieu" required>
                    <option value="">Sélectionner...</option>
                    <?php foreach ($refLieux as $l): ?>
                        <option value="<?= htmlspecialchars($l) ?>"><?= htmlspecialchars($l) ?></option>
                    <?php endforeach; ?>
                </select>
                <div class="field-error text-red-600 text-xs mt-1 hidden"></div>
            </div>

            <div>
                <label for="edit-statut" class="block text-sm font-medium text-gray-700 mb-1">Statut</label>
                <select id="edit-statut" name="statut" required class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
                    <option value="option">Option</option>
                    <option value="confirmee">Confirmée</option>
                    <option value="commandee">Commandée</option>
                </select>
                <div class="field-error text-red-600 text-xs mt-1 hidden"></div>
            </div>

            <div class="grid grid-cols-2 gap-4">
                <div>
                    <label for="edit-date-debut" class="block text-sm font-medium text-gray-700 mb-1">Date de début</label>
                    <input type="date" id="edit-date-debut" name="date_debut" required class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
                    <div class="field-error text-red-600 text-xs mt-1 hidden"></div>
                </div>
                <div>
                    <label for="edit-date-fin" class="block text-sm font-medium text-gray-700 mb-1">Date de fin</label>
                    <input type="date" id="edit-date-fin" name="date_fin" required class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
                    <div class="field-error text-red-600 text-xs mt-1 hidden"></div>
                </div>
            </div>

            <div class="grid grid-cols-2 gap-4">
                <div>
                    <label for="edit-heure-debut" class="block text-sm font-medium text-gray-700 mb-1">Heure de début</label>
                    <input type="time" id="edit-heure-debut" name="heure_debut" required class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
                    <div class="field-error text-red-600 text-xs mt-1 hidden"></div>
                </div>
                <div>
                    <label for="edit-heure-fin" class="block text-sm font-medium text-gray-700 mb-1">Heure de fin</label>
                    <input type="time" id="edit-heure-fin" name="heure_fin" required class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
                    <div class="field-error text-red-600 text-xs mt-1 hidden"></div>
                </div>
            </div>

            <div>
                <label for="edit-description" class="block text-sm font-medium text-gray-700 mb-1">Description (optionnel)</label>
                <textarea id="edit-description" name="description" rows="3" class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
                <div class="field-error text-red-600 text-xs mt-1 hidden"></div>
            </div>

            <div class="flex justify-end gap-2 pt-2">
                <button type="button" id="edit-cancel" class="px-4 py-2 border border-gray-300 rounded text-gray-700 hover:bg-gray-100">Annuler</button>
                <button type="submit" id="edit-submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-blue-300">Enregistrer</button>
            </div>
        </form>
    </div>
</div>

<script>
(function() {
    var modal = document.getElementById('edit-modal');
    var form = document.getElementById('edit-form');
    var errorBox = document.getElementById('edit-error');
    var submitBtn = document.getElementById('edit-submit');
    var tbody = document.getElementById('planning-body');

    var statutToColor = {
        'option':    'rgb(246, 191, 38)',
        'confirmee': 'rgb(121, 134, 203)',
        'commandee': 'rgb(213, 0, 0)'
    };
    var statutToLabel = {
        'option':    'Option',
        'confirmee': 'Confirmée',
        'commandee': 'Commandée'
    };
    var months = ['', 'janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin',
                  'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.'];

    function formatDates(start, end) {
        var s = new Date(start + 'T00:00:00');
        var e = new Date(end + 'T00:00:00');
        if (start === end) {
            return s.getDate() + ' ' + months[s.getMonth() + 1];
        }
        if (s.getMonth() === e.getMonth() && s.getFullYear() === e.getFullYear()) {
            return s.getDate() + ' → ' + e.getDate() + ' ' + months[e.getMonth() + 1];
        }
        return s.getDate() + ' ' + months[s.getMonth() + 1] + ' → ' + e.getDate() + ' ' + months[e.getMonth() + 1];
    }

    function setSelectValue(selectEl, value) {
        var ts = selectEl.tomselect;
        if (!ts) {
            selectEl.value = value;
            return;
        }
        if (value && !ts.options[value]) {
            ts.addOption({ value: value, text: value });
        }
        ts.setValue(value, true);
    }

    function clearFieldErrors() {
        form.querySelectorAll('.field-error').forEach(function(el) {
            el.textContent = '';
            el.classList.add('hidden');
        });
        errorBox.textContent = '';
        errorBox.classList.add('hidden');
    }

    function showFieldErrors(details) {
        Object.keys(details).forEach(function(field) {
            var input = form.querySelector('[name="' + field + '"]');
            if (!input) return;
            var errEl = input.parentElement.querySelector('.field-error');
            if (errEl) {
                errEl.textContent = details[field];
                errEl.classList.remove('hidden');
            }
        });
    }

    function showError(msg) {
        errorBox.textContent = msg;
        errorBox.classList.remove('hidden');
    }

    function openModal(row) {
        clearFieldErrors();
        modal.dataset.eventId = row.dataset.id;

        setSelectValue(document.getElementById('edit-formation'), row.dataset.formation || '');
        setSelectValue(document.getElementById('edit-client'),    row.dataset.client    || '');
        setSelectValue(document.getElementById('edit-lieu'),      row.dataset.lieu      || '');

        var statutKey = row.dataset.statutKey || '';
        document.getElementById('edit-statut').value = statutKey || 'option';

        document.getElementById('edit-date-debut').value  = row.dataset.start       || '';
        document.getElementById('edit-date-fin').value    = row.dataset.end         || '';
        document.getElementById('edit-heure-debut').value = row.dataset.heureDebut  || '09:00';
        document.getElementById('edit-heure-fin').value   = row.dataset.heureFin    || '17:00';
        document.getElementById('edit-description').value = row.dataset.notes       || '';

        modal.classList.remove('hidden');
        document.getElementById('edit-formation').focus();
    }

    function closeModal() {
        modal.classList.add('hidden');
        modal.dataset.eventId = '';
    }

    function updateRow(row, ev) {
        var colorIdToKey = { '5': 'option', '1': 'confirmee', '11': 'commandee' };
        var statutKey = colorIdToKey[ev.colorId] || '';

        var label = statutToLabel[statutKey] || '';
        var color = statutToColor[statutKey] || '';

        var formation = (ev.summary || '').split(' - ', 1)[0] || '';
        var summaryParts = (ev.summary || '').split(' - ');
        var client = summaryParts[1] || '';
        var lieu = ev.location || '';

        row.dataset.statut = label;
        row.dataset.statutKey = statutKey;
        row.dataset.formation = formation;
        row.dataset.client = client;
        row.dataset.lieu = lieu;
        row.dataset.start = ev.start || '';
        row.dataset.end = ev.endInclusive || '';
        row.dataset.heureDebut = ev.heure_debut || '';
        row.dataset.heureFin = ev.heure_fin || '';
        row.dataset.notes = ev.notes || '';

        var dot = row.querySelector('.status-dot');
        if (dot) dot.style.backgroundColor = color;
        var lbl = row.querySelector('.status-label');
        if (lbl) lbl.textContent = label;

        row.querySelector('.cell-formation').textContent = formation;
        row.querySelector('.cell-client').textContent = client;
        row.querySelector('.cell-lieu').textContent = lieu;
        row.querySelector('.cell-dates').textContent = formatDates(ev.start, ev.endInclusive);
        row.querySelector('.cell-horaires').textContent = (ev.heure_debut && ev.heure_fin) ? (ev.heure_debut + ' - ' + ev.heure_fin) : '';
    }

    tbody.addEventListener('click', function(e) {
        var btn = e.target.closest('.edit-btn');
        if (!btn) return;
        var row = btn.closest('tr');
        if (row) openModal(row);
    });

    document.getElementById('edit-close').addEventListener('click', closeModal);
    document.getElementById('edit-cancel').addEventListener('click', closeModal);
    modal.addEventListener('click', function(e) {
        if (e.target === modal) closeModal();
    });
    document.addEventListener('keydown', function(e) {
        if (e.key === 'Escape' && !modal.classList.contains('hidden')) closeModal();
    });

    form.addEventListener('submit', function(e) {
        e.preventDefault();
        clearFieldErrors();

        var eventId = modal.dataset.eventId;
        if (!eventId) return;

        var data = {
            formation:   document.getElementById('edit-formation').value,
            client:      document.getElementById('edit-client').value,
            lieu:        document.getElementById('edit-lieu').value,
            statut:      document.getElementById('edit-statut').value,
            date_debut:  document.getElementById('edit-date-debut').value,
            date_fin:    document.getElementById('edit-date-fin').value,
            heure_debut: document.getElementById('edit-heure-debut').value,
            heure_fin:   document.getElementById('edit-heure-fin').value,
            description: document.getElementById('edit-description').value
        };

        var originalText = submitBtn.textContent;
        submitBtn.disabled = true;
        submitBtn.textContent = 'Enregistrement…';

        fetch('/edit-event.php?id=' + encodeURIComponent(eventId), {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        }).then(function(res) {
            return res.json().then(function(json) { return { status: res.status, json: json }; });
        }).then(function(result) {
            if (result.status === 200) {
                var row = tbody.querySelector('tr[data-id="' + eventId + '"]');
                if (row) updateRow(row, result.json);
                closeModal();
                if (typeof window.applyPlanningFilters === 'function') {
                    window.applyPlanningFilters();
                }
            } else if (result.status === 422 && result.json.details) {
                showFieldErrors(result.json.details);
            } else {
                showError(result.json.error || 'Erreur inconnue');
            }
        }).catch(function(err) {
            showError('Erreur réseau : ' + err.message);
        }).finally(function() {
            submitBtn.disabled = false;
            submitBtn.textContent = originalText;
        });
    });

    // Apply Tom Select to the modal selects
    document.addEventListener('DOMContentLoaded', function() {
        ['edit-formation', 'edit-client', 'edit-lieu'].forEach(function(id) {
            new TomSelect('#' + id, {
                create: false,
                sortField: { field: 'text', direction: 'asc' }
            });
        });
    });
})();
</script>
```

- [ ] **Step 2: Expose the existing applyFilters function for re-use**

The existing filter/sort IIFE in `templates/planning.php` defines `applyFilters` locally. So it can be called from the edit IIFE after a row update, expose it on `window`.

In the existing filter/sort IIFE (the first `<script>` block in the file), find the line that begins `function applyFilters() {` and immediately after the line that closes the function body, just before `function restripe()`, the function definition is already complete. Then locate the bottom of that IIFE — the lines that read:

```javascript
    filterStatut.addEventListener('change', applyFilters);
    filterClient.addEventListener('change', applyFilters);
    filterFormation.addEventListener('change', applyFilters);
})();
```

Replace those four lines with:

```javascript
    filterStatut.addEventListener('change', applyFilters);
    filterClient.addEventListener('change', applyFilters);
    filterFormation.addEventListener('change', applyFilters);

    window.applyPlanningFilters = applyFilters;
})();
```

- [ ] **Step 3: Verify syntax**

Run: `php -l templates/planning.php`
Expected: `No syntax errors detected`

- [ ] **Step 4: Manual test**

Run: `php -S localhost:8080 -t public` in one terminal.
Open `http://localhost:8080/planning.php` in a browser.

Verify:
1. Each row has a "Modifier" button as the first column
2. Clicking "Modifier" opens the modal pre-filled with the row's data
3. Tom Select works on formation/client/lieu dropdowns
4. Submitting with valid data:
   - Closes the modal
   - Updates the row in place (statut color, text, dates, horaires)
   - The change is reflected in Google Calendar (refresh the page to confirm)
5. Submitting with invalid data shows inline field errors
6. Closing via ✕, "Annuler", clicking outside, or Échap all work
7. Filters and sort still work after an edit

- [ ] **Step 5: Commit**

```bash
git add templates/planning.php
git commit -m "feat: add edit modal with AJAX submission to planning view"
```

---

### Task 7: Build and verify

- [ ] **Step 1: Rebuild dist/**

Run: `bash build.sh`
Expected: `=== dist/ prêt ===`

- [ ] **Step 2: Verify new files are in dist/**

Run: `ls dist/public/edit-event.php dist/templates/planning.php`
Expected: Both files listed.

- [ ] **Step 3: Verify all tests still pass**

Run: `vendor/bin/phpunit`
Expected: All tests PASS.
