# Story: Tag Entity and Admin CRUD (Taxonomy for Actualité)

## 1. Contexte (Context)

The new "Actualité" (news article) page template requires a tagging system so admins can categorize news articles (e.g. multiple tags per article, rendered as pill badges in the hero and in related-post cards). Per the finalized spec (`spec-driven-development/2026-07-07-actualite-news-page-template.md`, Decisions #1 and #3), tags are implemented as a full `Tag` taxonomy **entity** — not free text, not a JSON string array — with a `ManyToMany` relation to `Page`, and require dedicated admin CRUD screens (list/create/edit/delete) following the project's View/API controller separation convention. This story is fully self-contained and has no dependency on any other Actualité story — it should be implemented first among the backend stories, since the Actualité DTO/Model story (tagIds validation) and the inline editor's tag picker both consume the `Tag` entity and its API.

## 2. Le besoin (User Story)

```
En tant qu'administrateur du site
Je souhaite créer, lister, modifier et supprimer des tags (étiquettes) réutilisables
Afin de catégoriser les articles d'actualité et permettre leur filtrage/affichage par tag
```

## 3. Liens utiles (Useful Links)

- **Figma**: https://www.figma.com/design/ldjPgBvJ6j2I9RVTa17H33/Galta_2026?node-id=146-1824 (node `146:1824`, "Detail-News_Galta" — see tag pill styling below the title)
- **Spec source**: `spec-driven-development/2026-07-07-actualite-news-page-template.md`, sections "Decisions" (#1, #3) and "Backend — Tags entity, relation & admin CRUD" task group
- **Architecture rules**:
  - `spec-driven-development/rules/data-transfer-objects.md` (mandatory `#[MapRequestPayload]`/`#[MapQueryString]` DTOs)
  - `spec-driven-development/rules/business-services.md` (business logic in dedicated Services, not Controllers)
  - `spec-driven-development/refacto-view-api-controller-separation.md` (ViewController vs ApiController separation)
- **Existing precedent to mirror**:
  - `src/Service/MediaFolderService.php` (delete-blocked-when-in-use pattern to reuse for tag deletion policy)
  - `src/Entity/MediaFolder.php` (UUID PK + uniqueness pattern)
  - Existing admin CRUD screens for `MediaFolder` or `Page` (visual/structural convention for `templates/admin/tags/*.html.twig`)
- CLAUDE.md — "Entity Structure", "Database Safety" (migration must be reviewed before `make db-migrate`)

## 4. Critères d'acceptation (Acceptance Criteria)

### Scénario 1: Création de l'entité Tag et de la migration
```gherkin
Scénario: L'entité Tag est créée avec contraintes d'unicité en base
  Étant donné que l'entité `src/Entity/Tag.php` est créée avec un UUID en clé primaire, un champ `name` et un champ `slug`
  Quand la migration Doctrine est générée via `make db-diff`
  Alors la migration ajoute uniquement la table `tag` et la table de jointure `page_tag` (relation ManyToMany avec Page), sans modification de schéma non liée
  Et la contrainte d'unicité sur `name` et `slug` est appliquée au niveau base de données (pas seulement au niveau applicatif)
  Et la migration est présentée à l'utilisateur pour revue avant exécution de `make db-migrate` (règle de sécurité base de données du projet)
```

### Scénario 2: Génération automatique du slug
```gherkin
Scénario: Le slug est généré automatiquement si non fourni
  Étant donné que je crée un tag avec uniquement un `name` renseigné
  Quand le tag est sauvegardé via TagManagementService::create()
  Alors le `slug` est généré automatiquement à partir du `name` (réutilisant l'utilitaire de slugification déjà utilisé par la création de Page)
  Et si un `slug` est explicitement fourni, il est utilisé tel quel (après validation d'unicité)
```

### Scénario 3: Validation d'unicité du nom et du slug
```gherkin
Scénario: La création d'un tag avec un nom déjà utilisé est rejetée
  Étant donné qu'un tag "Actualités" existe déjà
  Quand je tente de créer un nouveau tag avec le même `name` (ou le même `slug` résultant)
  Alors la création échoue avec une erreur de validation claire
  Et lors de la modification d'un tag existant, la vérification d'unicité exclut le tag lui-même (`TagRepository::existsByName(name, excludeId)`)
```

### Scénario 4: Politique de suppression d'un tag utilisé
```gherkin
Scénario: Un tag utilisé par une page publiée ne peut pas être supprimé
  Étant donné qu'un tag est assigné à au moins une page Actualité
  Quand un administrateur tente de le supprimer via `TagManagementService::delete()`
  Alors la suppression est bloquée avec un message d'erreur explicite (cohérent avec le pattern existant de `MediaFolderService` pour les dossiers non vides)

Scénario: Un tag non utilisé peut être supprimé
  Étant donné qu'un tag n'est assigné à aucune page
  Quand un administrateur le supprime
  Alors le tag est supprimé de la base de données sans erreur
```

### Scénario 5: Écrans d'administration CRUD (ViewController)
```gherkin
Scénario: L'administrateur gère les tags via les écrans HTML dédiés
  Étant donné que je suis connecté en tant qu'administrateur
  Quand j'accède à `GET /admin/tags`
  Alors je vois la liste de tous les tags triés par nom (`findAllOrderedByName()`)
  Quand j'accède à `GET /admin/tags/create` et soumets un formulaire valide
  Alors le tag est créé et je suis redirigé avec un message flash de succès
  Quand j'accède à `GET /admin/tags/{id}/edit` et modifie le nom
  Alors le tag est mis à jour et je suis redirigé avec un message flash de succès
  Et aucune de ces routes ne retourne de `JsonResponse` (HTML uniquement, pas de préfixe `/api/`)
```

### Scénario 6: Endpoints JSON pour le sélecteur de tags de l'éditeur (ApiController)
```gherkin
Scénario: Le picker de tags de l'éditeur inline consomme les endpoints JSON
  Étant donné que le `TagApiController` est implémenté
  Quand j'appelle `GET /api/admin/tags`
  Alors je reçois une réponse JSON `{success: true, data: [...]}` listant tous les tags
  Quand j'appelle `POST /api/admin/tags` avec un payload invalide (name manquant)
  Alors je reçois une réponse JSON `{success: false, error: "..."}` avec un code HTTP 400
  Quand j'appelle `PUT /api/admin/tags/{id}` ou `DELETE /api/admin/tags/{id}`
  Alors la réponse est toujours au format JSON `{success, data|error}`, jamais de `render()` ni de redirection
  Et toutes les routes de ce contrôleur sont préfixées par `/api/`
```

### Scénario 7: Utilisation des DTO et validation
```gherkin
Scénario: Les endpoints de création/modification utilisent des DTO validés
  Étant donné que `src/Dto/Tag/CreateTagDto.php` et `UpdateTagDto.php` existent avec `#[Assert\NotBlank]` sur `name`
  Quand le contrôleur API reçoit une requête POST/PUT
  Alors il utilise `#[MapRequestPayload]` pour mapper vers le DTO, sans jamais appeler `$request->request->get()` ou `json_decode($request->getContent())`
  Et le contrôleur ne fait qu'orchestrer : validation DTO → appel à `TagManagementService` → réponse HTTP (aucune logique métier ni manipulation d'entité dans le contrôleur)
```
