# Planning Table View — 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 a read-only page displaying all Google Calendar formations for the current year in a filterable table.

**Architecture:** Server-side rendering via `GoogleCalendarService::listEvents()`, client-side filtering in vanilla JS. New controller + template, one nav link added to layout. No new PHP classes.

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

---

## File Structure

| File | Action | Responsibility |
|------|--------|---------------|
| `templates/layout.php` | Modify | Add "Planning" nav link, support variable max-width via `$maxWidth` |
| `public/planning.php` | Create | Controller: fetch events, parse data, render template |
| `templates/planning.php` | Create | Template: table, filters, JS filtering logic |

---

### Task 1: Modify layout — nav link + flexible max-width

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

- [ ] **Step 1: Add the "Planning" nav link and flexible max-width**

In `templates/layout.php`, make two changes:

**Change 1** — Replace the hardcoded `max-w-4xl` in `<nav>` and `<main>` with a PHP variable that defaults to `max-w-4xl`:

Replace line 14:
```php
        <div class="max-w-4xl mx-auto px-4 py-3 flex items-center justify-between">
```
with:
```php
        <div class="<?= $maxWidth ?? 'max-w-4xl' ?> mx-auto px-4 py-3 flex items-center justify-between">
```

Replace line 25:
```php
    <main class="max-w-4xl mx-auto px-4">
```
with:
```php
    <main class="<?= $maxWidth ?? 'max-w-4xl' ?> mx-auto px-4">
```

**Change 2** — Add the "Planning" link in the nav, between "Nouvelle formation" and "Formations":

Replace lines 17-20:
```php
                <a href="/form.php" class="text-blue-600 hover:underline">Nouvelle formation</a>
                <a href="/admin/formations.php" class="text-gray-600 hover:underline">Formations</a>
```
with:
```php
                <a href="/form.php" class="text-blue-600 hover:underline">Nouvelle formation</a>
                <a href="/planning.php" class="text-gray-600 hover:underline">Planning</a>
                <a href="/admin/formations.php" class="text-gray-600 hover:underline">Formations</a>
```

- [ ] **Step 2: Verify existing pages still work**

Run: `php -l public/form.php && php -l templates/layout.php`
Expected: No syntax errors

Open `http://localhost:8080/form.php` in a browser and confirm the navigation displays correctly with the new "Planning" link (it will 404 for now, that's expected).

- [ ] **Step 3: Commit**

```bash
git add templates/layout.php
git commit -m "feat: add Planning nav link and flexible max-width to layout"
```

---

### Task 2: Create controller `public/planning.php`

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

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

Create `public/planning.php`:

```php
<?php

require_once __DIR__ . '/bootstrap.php';
$auth->requireAuth();

$gcal = getGoogleCalendarService();

$year = (int) date('Y');
$pageTitle = 'Planning ' . $year;
$maxWidth = 'max-w-6xl';
$events = [];
$error = null;

if (!$gcal->isAuthenticated()) {
    $error = 'Google Calendar non authentifié. Veuillez vous reconnecter via le formulaire de création.';
} else {
    try {
        $rawEvents = $gcal->listEvents($year . '-01-01', $year . '-12-31');
    } catch (\Throwable $e) {
        error_log('Planning error: ' . $e->getMessage());
        $rawEvents = [];
        $error = 'Erreur lors de la récupération des événements.';
    }

    $colorToStatut = [
        '5'  => ['label' => 'Option',    'color' => 'rgb(246, 191, 38)'],
        '1'  => ['label' => 'Confirmée', 'color' => 'rgb(121, 134, 203)'],
        '11' => ['label' => 'Commandée', 'color' => 'rgb(213, 0, 0)'],
    ];

    $months = ['', 'janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin',
               'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.'];

    foreach ($rawEvents as $ev) {
        // Parse summary: "Formation - Client - Lieu"
        $parts = explode(' - ', $ev['summary'] ?? '', 3);
        $formation = trim($parts[0] ?? '');
        $client = trim($parts[1] ?? '');
        $lieu = $ev['location'] ?? '';

        // Statut from colorId
        $colorId = (string) ($ev['colorId'] ?? '');
        $statutInfo = $colorToStatut[$colorId] ?? null;
        $statutLabel = $statutInfo ? $statutInfo['label'] : '';
        $statutColor = $statutInfo ? $statutInfo['color'] : '';

        // Dates — end is exclusive in Google Calendar, subtract 1 day
        $startDate = $ev['start'] ?? '';
        $endRaw = $ev['end'] ?? '';
        if ($endRaw !== '' && strlen($endRaw) === 10) {
            $endDate = (new DateTimeImmutable($endRaw))->modify('-1 day')->format('Y-m-d');
        } else {
            $endDate = $startDate;
        }

        // Format dates for display
        if ($startDate === $endDate) {
            $startDt = new DateTimeImmutable($startDate);
            $datesDisplay = (int) $startDt->format('j') . ' ' . $months[(int) $startDt->format('n')];
        } else {
            $startDt = new DateTimeImmutable($startDate);
            $endDt = new DateTimeImmutable($endDate);
            if ($startDt->format('n') === $endDt->format('n')) {
                $datesDisplay = (int) $startDt->format('j') . ' → ' . (int) $endDt->format('j') . ' ' . $months[(int) $endDt->format('n')];
            } else {
                $datesDisplay = (int) $startDt->format('j') . ' ' . $months[(int) $startDt->format('n')]
                    . ' → ' . (int) $endDt->format('j') . ' ' . $months[(int) $endDt->format('n')];
            }
        }

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

        $events[] = [
            'formation'    => $formation,
            'client'       => $client,
            'lieu'         => $lieu,
            'statutLabel'  => $statutLabel,
            'statutColor'  => $statutColor,
            'startDate'    => $startDate,
            'datesDisplay' => $datesDisplay,
            'horaires'     => $horaires,
        ];
    }

    // Sort by start date ascending
    usort($events, fn($a, $b) => $a['startDate'] <=> $b['startDate']);
}

ob_start();
require __DIR__ . '/../templates/planning.php';
$content = ob_get_clean();
require __DIR__ . '/../templates/layout.php';
```

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

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

- [ ] **Step 3: Commit**

```bash
git add public/planning.php
git commit -m "feat: add planning page controller"
```

---

### Task 3: Create template `templates/planning.php`

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

- [ ] **Step 1: Create the template with table, filters, and JS**

Create `templates/planning.php`:

```php
<h1 class="text-2xl font-bold text-gray-800 mb-6"><?= htmlspecialchars($pageTitle) ?></h1>

<?php if ($error): ?>
    <div class="bg-yellow-100 text-yellow-800 p-4 rounded mb-4">
        <?= htmlspecialchars($error) ?>
    </div>
<?php elseif (empty($events)): ?>
    <div class="bg-gray-100 text-gray-600 p-4 rounded mb-4">
        Aucune formation trouvée pour l'année <?= $year ?>.
    </div>
<?php else: ?>

<?php
    // Build unique sorted lists for filters
    $filterFormations = array_unique(array_column($events, 'formation'));
    $filterClients = array_unique(array_column($events, 'client'));
    sort($filterFormations, SORT_LOCALE_STRING);
    sort($filterClients, SORT_LOCALE_STRING);
    $filterFormations = array_filter($filterFormations, fn($v) => $v !== '');
    $filterClients = array_filter($filterClients, fn($v) => $v !== '');
?>

<div class="flex flex-wrap gap-4 mb-4">
    <div>
        <label for="filter-statut" class="block text-sm font-medium text-gray-600 mb-1">Statut</label>
        <select id="filter-statut" class="border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
            <option value="">Tous</option>
            <option value="Option">Option</option>
            <option value="Confirmée">Confirmée</option>
            <option value="Commandée">Commandée</option>
        </select>
    </div>
    <div>
        <label for="filter-client" class="block text-sm font-medium text-gray-600 mb-1">Client</label>
        <select id="filter-client" class="border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
            <option value="">Tous</option>
            <?php foreach ($filterClients as $c): ?>
                <option value="<?= htmlspecialchars($c) ?>"><?= htmlspecialchars($c) ?></option>
            <?php endforeach; ?>
        </select>
    </div>
    <div>
        <label for="filter-formation" class="block text-sm font-medium text-gray-600 mb-1">Formation</label>
        <select id="filter-formation" class="border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
            <option value="">Tous</option>
            <?php foreach ($filterFormations as $f): ?>
                <option value="<?= htmlspecialchars($f) ?>"><?= htmlspecialchars($f) ?></option>
            <?php endforeach; ?>
        </select>
    </div>
</div>

<p id="event-count" class="text-sm text-gray-500 mb-4"><?= count($events) ?> formation(s) affichée(s)</p>

<div class="bg-white shadow rounded-lg overflow-hidden">
    <table class="w-full">
        <thead class="bg-gray-50">
            <tr>
                <th class="text-left py-2 px-4 font-medium text-gray-600 text-sm">Statut</th>
                <th class="text-left py-2 px-4 font-medium text-gray-600 text-sm">Formation</th>
                <th class="text-left py-2 px-4 font-medium text-gray-600 text-sm">Client</th>
                <th class="text-left py-2 px-4 font-medium text-gray-600 text-sm">Lieu</th>
                <th class="text-left py-2 px-4 font-medium text-gray-600 text-sm">Dates</th>
                <th class="text-left py-2 px-4 font-medium text-gray-600 text-sm">Horaires</th>
            </tr>
        </thead>
        <tbody id="planning-body">
            <?php foreach ($events as $i => $ev): ?>
            <tr class="<?= $i % 2 === 0 ? 'bg-white' : 'bg-gray-50' ?> border-t border-gray-100"
                data-statut="<?= htmlspecialchars($ev['statutLabel']) ?>"
                data-client="<?= htmlspecialchars($ev['client']) ?>"
                data-formation="<?= htmlspecialchars($ev['formation']) ?>">
                <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; ?>
        </tbody>
    </table>
</div>

<script>
(function() {
    var filterStatut = document.getElementById('filter-statut');
    var filterClient = document.getElementById('filter-client');
    var filterFormation = document.getElementById('filter-formation');
    var rows = document.querySelectorAll('#planning-body tr');
    var countEl = document.getElementById('event-count');

    function applyFilters() {
        var statut = filterStatut.value;
        var client = filterClient.value;
        var formation = filterFormation.value;
        var visible = 0;

        rows.forEach(function(row) {
            var matchStatut = !statut || row.getAttribute('data-statut') === statut;
            var matchClient = !client || row.getAttribute('data-client') === client;
            var matchFormation = !formation || row.getAttribute('data-formation') === formation;

            if (matchStatut && matchClient && matchFormation) {
                row.style.display = '';
                visible++;
            } else {
                row.style.display = 'none';
            }
        });

        countEl.textContent = visible + ' formation(s) affichée(s)';
    }

    filterStatut.addEventListener('change', applyFilters);
    filterClient.addEventListener('change', applyFilters);
    filterFormation.addEventListener('change', applyFilters);
})();
</script>

<?php endif; ?>
```

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

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

- [ ] **Step 3: Test in browser**

Open `http://localhost:8080/planning.php` in a browser.

Verify:
1. The page displays the "Planning 2026" title
2. The navigation shows the new "Planning" link
3. The table displays all formations with correct columns
4. The filters work: selecting a statut/client/formation hides non-matching rows
5. The counter updates when filtering
6. Dates display correctly (single day: "15 avr.", multi-day: "15 → 17 avr.")
7. Status badges show correct colors (yellow for Option, lavender for Confirmée, red for Commandée)

- [ ] **Step 4: Commit**

```bash
git add templates/planning.php
git commit -m "feat: add planning table view template with filters"
```

---

### Task 4: Build and deploy

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

Run: `bash build.sh`
Expected: `dist/` folder is recreated with all files including the new `public/planning.php` and `templates/planning.php`

- [ ] **Step 2: Verify dist contains new files**

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

- [ ] **Step 3: Commit build output if build.sh was updated**

No build.sh changes needed — existing script already copies `public/` and `templates/`.
