# Coach Fitness — site vitrine & back-office

Site complet pour un coach sportif indépendant : présentation de l'activité, prise de
rendez-vous en ligne, blog, et back-office permettant de piloter aussi bien les contenus
que l'apparence du site.

**Stack** : Laravel 13 · Inertia 2 · Vue 3 · Tailwind CSS 4 · Filament 5 · MySQL 8.4 · Laravel Sail

---

## Démarrage

```bash
cd /var/www/coach-fitness
./vendor/bin/sail up -d          # démarre PHP, MySQL, Redis et Mailpit
./vendor/bin/sail npm run dev    # serveur Vite (rechargement à chaud)
```

| Service | URL |
| --- | --- |
| Site public | http://localhost:8090 |
| Back-office | http://localhost:8090/admin |
| Mailpit (e-mails sortants) | http://localhost:8026 |
| MySQL (depuis l'hôte) | `127.0.0.1:3308` — base `coach_fitness`, user `sail` / `password` |

Les ports sont volontairement décalés (`8090`, `3308`, `6381`, `8026`) car le port 80 de la
machine est occupé par Apache et d'autres projets utilisent déjà les ports Sail par défaut.

### Compte de démonstration

| E-mail | Mot de passe |
| --- | --- |
| `coach@coach-fitness.test` | `password` |

Ces valeurs se changent par `ADMIN_EMAIL`, `ADMIN_NAME` et `ADMIN_PASSWORD` dans le fichier
`.env`. En production, le mot de passe de démonstration ne s'applique jamais : voir
[DEPLOIEMENT.md](DEPLOIEMENT.md).

Seuls les comptes dont la colonne `users.is_admin` vaut `true` accèdent au back-office.

### Réinstaller depuis zéro

```bash
./vendor/bin/sail artisan migrate:fresh --seed
```

Hors production, cette commande réinstalle aussi les contenus de démonstration — prestations,
témoignages, articles, rendez-vous. C'est ce qui fait de ce dépôt une vitrine présentable.
Chez un vrai client, ces contenus ne sont jamais installés.

---

## Ce que gère le back-office

| Rubrique | Contenu |
| --- | --- |
| **Agenda** | Rendez-vous (confirmer, annuler, marquer honoré/absent), plages horaires hebdomadaires, congés et exceptions, messages reçus |
| **Contenu du site** | Prestations et tarifs, témoignages, questions fréquentes |
| **Blog** | Articles (éditeur riche, brouillons, SEO) et catégories |
| **Configuration** | Identité, apparence, page d'accueil, coordonnées, règles de réservation, rappels automatiques, référencement, confidentialité |
| **Pages libres** | Mentions légales, CGV, politique de confidentialité : rédigées en back-office, avec variables tenues à jour automatiquement |

Le tableau de bord affiche les demandes à traiter, les séances de la semaine et du mois,
les messages non lus, ainsi que la liste des prochains rendez-vous avec confirmation en un clic.

---

## Personnalisation de l'apparence

`Configuration → Apparence et contenus` permet de modifier, sans toucher au code :

- **Couleurs** — principale, sombre, accentuation, fond, texte, texte secondaire.
  Les nuances dérivées (survol, fonds clairs, couleur de texte contrastée) sont calculées
  automatiquement, y compris le choix noir/blanc selon la luminance pour rester lisible.
- **Typographie** — douze polices servies par Bunny Fonts (sans cookie, conforme RGPD).
- **Formes** — style des boutons (anguleux / arrondis / pilule), arrondi des blocs, largeur du contenu.
- **Logo, favicon, images** — logo clair et sombre, image de partage sociale, photos d'accueil.
- **Textes** — titres et accroches de la page d'accueil, chiffres clés, appels à l'action.

### Comment ça fonctionne

`App\Support\Theme` traduit les réglages en variables CSS `--brand-*`, injectées côté serveur
dans le `<head>` — il n'y a donc aucun clignotement de couleurs au chargement. Le CSS déclare
ces variables en `@theme inline` (Tailwind 4), si bien que les utilitaires `bg-brand`,
`text-muted` ou `rounded-button` pointent vers `var(--brand-*)` : **changer une couleur en
back-office repeint le site sans recompiler les assets**.

Pour ajouter un réglage : déclarez sa valeur par défaut dans `SiteSettings::defaults()`,
ajoutez le champ correspondant dans `App\Filament\Pages\SiteSettings`, et consommez-le
via `settings('groupe.cle')` côté PHP ou `page.props.site` côté Vue.

---

## Le moteur de rendez-vous

Trois sources se combinent pour produire les créneaux proposés :

1. **Plages hebdomadaires** (`availability_rules`) — récurrentes, générales ou propres à une
   prestation. Si une prestation définit ses propres plages, elles priment sur les plages générales.
2. **Exceptions datées** (`availability_exceptions`) — fermetures (congés, formation) ou
   ouvertures ponctuelles, sur la journée entière ou sur une tranche horaire.
3. **Rendez-vous existants** — un rendez-vous qui chevauche le créneau mobilise le coach et
   rend le créneau indisponible. À horaire et prestation identiques, c'est la capacité de la
   prestation qui s'applique — ce qui permet les cours collectifs.

Les réglages `booking.*` bornent l'ensemble : pas de réservation avant le délai de prévenance,
ni au-delà de l'horizon, avec un battement optionnel entre deux séances.

### Protection contre la double réservation

Chaque réservation occupe une **place** numérotée sur son créneau, garantie par l'index unique
`(service_id, starts_at, slot_seat)`. Deux demandes simultanées ne peuvent donc pas obtenir la
même place : la seconde reçoit une violation de contrainte et `BookingService` retente sur la
place suivante, jusqu'à épuisement de la capacité. À l'annulation, `slot_seat` repasse à `null`
— MySQL tolérant plusieurs `null` dans un index unique, la place est immédiatement réutilisable.

### Parcours client

Réservation en trois étapes (prestation → créneau → coordonnées), puis e-mail de récapitulatif
au client et de notification au coach. Chaque e-mail contient un lien d'annulation protégé par
un jeton de 48 caractères comparé en temps constant.

Les e-mails partent en **file d'attente** : une panne SMTP ne ralentit ni ne fait échouer une
réservation déjà enregistrée. Une tâche qui échoue est journalisée et consultable par
`php artisan queue:failed`.

Un **rappel automatique** part avant la séance, seulement pour les rendez-vous confirmés, une
seule fois par rendez-vous (`reminder_sent_at`). Activation et délai se règlent dans
`Configuration → Réservation → Rappel automatique`.

---

## Référencement

Le site est une application Inertia **sans rendu serveur** : les balises posées par le
composant `<Head>` de Vue n'existent qu'une fois le JavaScript exécuté. Les robots de
Facebook, LinkedIn ou WhatsApp ne l'exécutent pas — un partage n'afficherait donc que le
titre générique du site.

Chaque contrôleur construit donc sa fiche via `App\Support\Seo`, et la vue racine l'écrit
dans le HTML livré : titre, description, lien canonique, balises Open Graph et Twitter, et
données structurées schema.org.

| Élément | Source |
| --- | --- |
| Fiche d'établissement (`HealthAndBeautyBusiness`) | Coordonnées, réseaux sociaux et **plages horaires** saisis en back-office |
| `Service` avec tarif et disponibilité | Fiche prestation |
| `BlogPosting` avec dates, rubrique et auteur | Article |
| `FAQPage` | Questions fréquentes de l'accueil |
| Image de partage | Couverture de l'article ou photo de la prestation, à défaut l'image de partage du site |

`/sitemap.xml` et `/robots.txt` sont générés à la demande depuis le contenu publié : une
rubrique désactivée en back-office en disparaît d'elle-même. Le réglage
`Référencement → Autoriser les moteurs de recherche` ferme le site aux robots le temps de le
remplir — la consigne vaut alors pour `robots.txt` comme pour chaque page.

Les pages sans intérêt pour un index (recherche dans le blog, confirmation de rendez-vous,
annulation) se déclarent elles-mêmes en `noindex`.

### Icône du site

Tant qu'aucun favicon n'est déposé en back-office, `App\Support\Favicon` en dessine un aux
couleurs du thème, servi en SVG, PNG et ICO. Le dessin est mis en cache et son empreinte sert
d'ETag : il se régénère dès que le coach change de couleur.

---

## Structure du code

```
app/
├── Enums/              AppointmentStatus, Weekday (libellés et couleurs pour Filament)
├── Exceptions/         SlotUnavailableException
├── Filament/
│   ├── Pages/          SiteSettings — la page de personnalisation
│   ├── Resources/      Une ressource par entité métier
│   └── Widgets/        Tableau de bord
├── Http/
│   ├── Controllers/    Pages publiques, réservation, contact
│   ├── Requests/       Validation (avec champ leurre anti-robot)
│   └── Resources/      Sérialisation vers Inertia
├── Console/Commands/   Rappels de rendez-vous · purge RGPD · synchronisation des polices
├── Mail/               Six courriels transactionnels, tous mis en file d'attente
├── Models/
├── Services/           AvailabilityService (créneaux) · BookingService (écriture)
└── Support/            SiteSettings · Theme · Seo · Favicon · PageContent · Privacy · Icons

resources/js/
├── Components/         AppIcon, cartes, accordéon FAQ, en-tête, pied de page…
├── Layouts/            PublicLayout
└── Pages/              Home, About, Services/*, Blog/*, Booking/*, Contact, ContentPage

resources/views/
├── errors/             Pages 403/404/419/429/500/503 à la charte, sans JavaScript
├── partials/           Balises de référencement et icônes, rendues côté serveur
└── mail/               Gabarits des courriels
```

---

## Tests

```bash
./vendor/bin/sail artisan test
```

87 tests couvrent les pages publiques, le filtrage du blog, le formulaire de contact
(consentement et anti-robot), le moteur de créneaux (délai de prévenance, horizon, fermetures
totales et partielles), la double réservation, les cours collectifs, l'annulation par jeton,
l'accès restreint à la page de confirmation, les rappels automatiques, le référencement rendu
côté serveur, le plan du site, l'icône générée, les droits d'accès au back-office et la
propagation d'un changement de thème jusqu'au site public.

La suite tourne aussi bien sur MySQL que sur SQLite, la configuration de test par défaut de
Laravel :

```bash
DB_CONNECTION=sqlite DB_DATABASE=:memory: php artisan test
```

---

## Mise en production

Le déploiement chez un client réel — prérequis de l'hébergement, informations à réclamer,
installation, paramétrage, vérifications et dépannage — fait l'objet d'un document à part :

**→ [DEPLOIEMENT.md](DEPLOIEMENT.md)**

Trois points y décident du reste, autant les connaître dès maintenant :

- **Un cron à la minute est obligatoire.** Le site enregistre les rendez-vous, mais les
  confirmations, rappels et notifications partent par une file d'attente. Sans
  `schedule:run`, aucun e-mail ne part. Certains hébergements mutualisés ne le proposent pas :
  le vérifier avant de vendre.
- **`TRUSTED_PROXIES` derrière un reverse proxy.** Sans ce réglage, l'application ne voit que
  du HTTP en clair et fabrique des URL en `http://` — liens d'annulation envoyés par e-mail
  compris. Le symptôme se lit dans `sitemap.xml`.
- **`db:seed` n'installe pas les contenus de démonstration en production.** Le site d'un vrai
  client démarre vide : de faux témoignages sont une pratique commerciale trompeuse.

---

## Ce que le site ne fait pas

- **Aucun bandeau de consentement** : le choix est assumé — le site ne dépose que deux cookies
  techniques, exemptés de consentement. Il impose en contrepartie un outil de mesure sans
  cookie (Plausible, Fathom, Matomo sans cookie). Le back-office alerte si un script à
  traceurs est collé. Utiliser Google Analytics exigerait d'ajouter un bandeau.
- **Pas d'intégration d'encaissement** : les paiements se règlent hors ligne.
- **Pas de rendu serveur (SSR)** : les métadonnées sont rendues côté serveur, mais le contenu
  des pages reste client. Google l'indexe ; d'autres moteurs plus rudimentaires peuvent ne pas
  le faire.
- **Pas de gestion multi-coachs** : un site, un professionnel.
