chore(tickets): met à jour le suivi du ticket #83 (lot backend)

Lot backend (tags + duplication) commité et vérifié (110 tests, 1 échec préexistant sans rapport). Lot frontend F1/F2 à venir avant clôture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 10:05:04 +02:00
parent add5025dd2
commit e57cfdf54d
3 changed files with 784 additions and 5 deletions

View File

@ -1,6 +1,785 @@
---
issueRef: "#83"
version: 1
version: 5
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784650720801
updatedAt: 1784707484092
---
# UX — Duplication rapide + tags de filtrage
## Contexte lu
Ticket #83 : besoin commercial moyen terme pour accélérer l'adaptation de programmes/séances existants et garder les bibliothèques exploitables quand elles grossissent.
Mémos pris en compte :
- `gametime-ux-conception` : listes simples, badges de conditions, sections progressives, pas de surcharge.
- `gametime-visual-identity` : Court Blazer, cartes plates, rayon 6 px, badges compacts rayon 5 px, sobriété.
- `gametime-ux-execution-nav-and-program-simplification` : favoriser les écrans dédiés quand une édition devient dense ; les listes doivent rester compactes.
État actuel observé dans le code :
- `exercise_library_screen.dart` : liste d'exercices avec recherche, filtres par mesures, badge `Exemple`, badge catégorie, badges mesures, icône média, suppression visible.
- `program_screen.dart` : liste simple de programmes, badge `Exemple`, résumé, suppression visible, pas encore de recherche/filtre.
- `workout_template_screen.dart` : liste simple de séances, badge `Exemple`, résumé, bouton visible `Lancer`, suppression visible, pas encore de recherche/filtre.
- Les catégories d'exercice existent déjà (`Tir`, `Dribble`, etc.). Les tags ne doivent pas remplacer cette catégorie principale : ils ajoutent une couche de filtrage libre et transverse.
## Décision générale
Ajouter deux motifs transverses légers :
1. **Duplication** uniquement pour `Programme` et `Séance-modèle`, via menu contextuel `...` sur les cartes/list items.
2. **Tags** sur `Exercice`, `Programme`, `Séance-modèle`, affichés comme badges compacts et filtrables par chips en haut des listes.
Ne pas introduire :
- hiérarchie de tags ;
- catégories imbriquées ;
- écran global de gestion des tags ;
- swipe actions ;
- dashboard de bibliothèque.
## Duplication
### Où placer l'action
#### Liste Programmes
Remplacer la suppression visible par un menu contextuel pour éviter de multiplier les icônes dans le trailing.
Structure de ligne cible :
```text
Programme tirs extérieur [Exemple]
6 exercices · 18 séries
[tag] [tag]
[...] [>]
```
Menu `...` :
```text
Dupliquer
Supprimer
```
Si le partage est disponible depuis la liste plus tard, ordre recommandé :
```text
Dupliquer
Partager
Supprimer
```
Raison : `Dupliquer` est utile mais pas l'action principale de la liste ; le tap principal reste `Modifier`. `Supprimer` ne doit pas rester l'icône la plus visible.
#### Liste Séances
Conserver `Lancer` visible, car c'est l'action principale d'une séance.
Structure de ligne cible :
```text
Prépa match [Exemple]
2 programmes · 11 exercices
[tag] [tag]
[Lancer] [...] [>]
```
Menu `...` :
```text
Dupliquer
Supprimer
```
Si partage disponible depuis la liste plus tard :
```text
Dupliquer
Partager
Supprimer
```
Ne pas mettre `Dupliquer` en bouton visible sur chaque ligne : la liste deviendrait trop chargée et `Lancer` doit rester prioritaire sur les séances.
### Comportement de duplication Programme
Action : menu `Dupliquer` sur un programme.
Comportement attendu :
- créer immédiatement un nouveau programme ;
- nom généré : `Copie de <nom du programme>` ;
- si ce nom existe déjà, utiliser `Copie 2 de <nom>`, puis `Copie 3 de <nom>` ;
- copier tous les exercices du programme, dans le même ordre ;
- copier les snapshots d'exercice utilisés par le programme ;
- copier nombre de séries, mesures activées, cibles, repos, réglage d'enchaînement d'étapes ;
- copier les tags du programme ;
- ne pas copier le badge `Exemple` : une copie utilisateur n'est pas un exemple ;
- ne garder aucun lien fonctionnel vers l'original. Modifier l'original ou la copie ensuite ne modifie pas l'autre.
Après succès : ouvrir directement l'écran d'édition de la copie.
Feedback au moment de l'ouverture : snackbar discrète sur l'écran d'édition :
```text
Programme dupliqué. Ajuste la copie avant de l'utiliser.
```
Titre de l'écran ouvert :
```text
Modifier le programme
```
Le champ nom contient déjà :
```text
Copie de Programme tirs extérieur
```
Si échec : snackbar liste ou formulaire :
```text
Duplication impossible pour le moment.
```
### Comportement de duplication Séance-modèle
Action : menu `Dupliquer` sur une séance.
Comportement attendu :
- créer immédiatement une nouvelle séance-modèle ;
- nom généré : `Copie de <nom de la séance>` ;
- si conflit : `Copie 2 de <nom>`, `Copie 3 de <nom>` ;
- copier tous les programmes intégrés à la séance avec leurs snapshots ;
- copier les overrides locaux de séance : nombre de séries et valeurs cibles sur les programmes intégrés ;
- copier l'ordre des programmes ;
- copier les tags de la séance ;
- ne pas copier `lastStartedAt` ;
- ne pas copier le badge `Exemple` ;
- ne garder aucun lien fonctionnel vers la séance originale.
Après succès : ouvrir directement l'écran d'édition de la copie.
Feedback :
```text
Séance dupliquée. Ajuste la copie avant de la lancer.
```
Si échec :
```text
Duplication impossible pour le moment.
```
### Pas de duplication d'exercice au MVP
Le ticket demande explicitement la duplication de programmes et séances-modèles. Ne pas ajouter `Dupliquer` aux exercices au MVP, pour garder le lot ciblé. Les exercices ont déjà une création riche et une logique de snapshots dans les programmes ; dupliquer les exercices pourra être cadré séparément si demandé.
## Tags
### Modèle UX
Un tag est un libellé libre court, commun aux trois bibliothèques : exercices, programmes, séances.
Exemples :
```text
extérieur
match
dribble
sans matériel
intense
10 min
```
Règles MVP :
- 0 à 8 tags par objet ;
- 24 caractères max par tag ;
- pas de couleur personnalisée par tag ;
- pas de catégories de tags ;
- pas de hiérarchie ;
- normalisation visuelle en minuscules recommandée, sauf si Architect préfère conserver la casse saisie ;
- doublons interdits sur un même objet (`tir` et `Tir` comptent comme le même tag).
Le tag ne remplace pas la catégorie d'exercice. Exemple : un exercice peut avoir catégorie `Tir` et tags `extérieur`, `match`, `fatigue`.
### Affichage des tags sur les listes
#### Exercices
Titre actuel conservé : nom + badge `Exemple`.
Sous-titre : description éventuelle, puis badges.
Ordre des badges :
```text
[Catégorie] [Temps] [Score] [tag] [tag] [média]
```
Règles :
- catégorie existante conservée ;
- badges mesures conservés ;
- tags affichés après catégorie/mesures ;
- max 3 tags visibles par ligne d'item ;
- au-delà : badge `+2` ;
- badge tag style compact, proche `ExampleBadge` : bordure, rayon 5 px, texte secondaire ; pas de couleur forte.
#### Programmes
Ajouter les tags dans une deuxième ligne compacte sous le résumé si l'objet en a.
```text
Programme tirs extérieur [Exemple]
6 exercices · 18 séries
[extérieur] [match]
```
Si aucun tag : ne pas réserver d'espace.
#### Séances
Même règle : tags sous le résumé, sans concurrencer `Lancer`.
```text
Prépa match [Exemple]
2 programmes · 11 exercices
[intense] [match]
```
### Édition des tags
Ajouter une section `Tags` dans les écrans de création/édition.
#### Exercice
Placement : après `Description`, avant `Média optionnel`.
```text
Tags
Ajoute quelques mots pour retrouver cet exercice plus vite.
[ extérieur ] [ match ]
[Ajouter un tag]
```
Le champ catégorie d'exercice, s'il est rendu éditable dans le même lot, reste séparé sous une section `Organisation` :
```text
Organisation
Catégorie
[Tir v]
Tags
...
```
Mais le tag MVP ne dépend pas de cette évolution.
#### Programme
Placement : après `Repos par défaut (s)`, avant `Ajouter un exercice`.
```text
Tags
[ extérieur ] [ match ]
[Ajouter un tag]
```
#### Séance-modèle
Placement : après `Nom`, avant `Ajouter un programme`.
```text
Tags
[ match ] [ intense ]
[Ajouter un tag]
```
### Interaction d'ajout de tag
Action `Ajouter un tag` ouvre une bottom sheet simple ou un champ inline selon ce qui est le plus rapide à implémenter proprement. Recommandation UX : bottom sheet courte pour réutiliser les suggestions.
Titre :
```text
Ajouter un tag
```
Champ :
```text
Nom du tag
```
Suggestions sous le champ, si des tags existent déjà dans la bibliothèque :
```text
Tags existants
[extérieur] [match] [dribble]
```
Actions :
```text
[Annuler]
[Ajouter]
```
Validation :
- vide : `Saisis un tag.`
- trop long : `Un tag contient 24 caractères maximum.`
- doublon sur l'objet : `Ce tag est déjà ajouté.`
- limite atteinte : `Tu peux ajouter jusqu'à 8 tags.`
Suppression d'un tag : chaque chip éditable a une icône de retrait `x` ou action delete accessible. Pas de confirmation.
Ne pas supprimer globalement un tag depuis un objet : retirer le tag de l'objet seulement.
## Filtrage par tags
### Principe
Chaque liste affiche une zone de filtre en haut.
Exercices : conserver la recherche et les filtres mesures existants, ajouter les tags dessous.
```text
[Rechercher]
Mesures
[Temps] [Répétitions] [Score]
Tags
[extérieur] [match] [intense]
```
Programmes : ajouter recherche simple + tags.
```text
[Rechercher]
Tags
[extérieur] [match] [intense]
```
Séances : ajouter recherche simple + tags.
```text
[Rechercher]
Tags
[match] [routine] [intense]
```
La recherche reste simple : nom + résumé court si disponible. Pas de syntaxe avancée.
### Sélection des tags
- Chips `FilterChip`, multi-sélection.
- Logique : l'objet doit contenir **tous** les tags sélectionnés, comme les filtres mesures d'exercices qui réduisent la liste.
- Si aucun tag n'existe dans une bibliothèque, ne pas afficher la section `Tags`.
- Les tags sont triés par usage décroissant puis alphabétique en cas d'égalité.
- Limiter l'affichage initial à 12 tags maximum ; si plus, afficher action :
```text
Voir tous les tags
```
qui ouvre une bottom sheet de sélection avec recherche simple `Rechercher un tag`.
### Réinitialisation
Quand un filtre est actif, afficher une action discrète :
```text
Effacer les filtres
```
Cette action efface recherche + tags + mesures sur exercices.
## États vides
### Bibliothèque vide
Conserver les messages actuels.
Exercices :
```text
Aucun exercice
Crée ton premier exercice ou restaure les exemples.
[Créer un exercice]
```
Programmes :
```text
Aucun programme
Assemble quelques exercices pour préparer une séance.
[Créer un programme]
```
Séances :
```text
Aucune séance
Crée une séance-modèle pour lancer ton entraînement plus vite.
[Créer une séance]
```
### Aucun résultat après filtre
Exercices :
```text
Aucun exercice ne correspond
Modifie la recherche, les mesures ou les tags sélectionnés.
[Effacer les filtres]
```
Programmes :
```text
Aucun programme ne correspond
Modifie la recherche ou les tags sélectionnés.
[Effacer les filtres]
```
Séances :
```text
Aucune séance ne correspond
Modifie la recherche ou les tags sélectionnés.
[Effacer les filtres]
```
### Aucun tag disponible
Ne pas afficher une section vide `Tags`. Dans les formulaires, afficher seulement `Ajouter un tag`.
### Duplication en cours
Si la duplication est async et visible : désactiver le menu de l'item concerné ou afficher un petit loader dans le menu si simple.
Pas de dialog de progression.
### Duplication réussie mais ouverture impossible
Cas rare : copie créée mais navigation vers l'édition échoue.
Snackbar :
```text
Copie créée.
```
La liste se recharge et la copie apparaît.
## Libellés exacts
Actions :
- `Dupliquer`
- `Supprimer`
- `Lancer`
- `Créer`
- `Enregistrer`
- `Ajouter un tag`
- `Effacer les filtres`
- `Voir tous les tags`
Feedback :
- `Programme dupliqué. Ajuste la copie avant de l'utiliser.`
- `Séance dupliquée. Ajuste la copie avant de la lancer.`
- `Duplication impossible pour le moment.`
- `Ce tag est déjà ajouté.`
- `Tu peux ajouter jusqu'à 8 tags.`
À éviter :
- `Clone`
- `Fork`
- `Template`
- `Taxonomie`
- `Libellé système`
- `Tag global supprimé`
## Exigences de données pour Architect
### Tags
Prévoir un modèle simple, sync-friendly, local-first :
- tags sur `Exercise`, `Program`, `WorkoutTemplate` ;
- libellé utilisateur affichable ;
- normalisation pour comparaison/doublons ;
- ordre d'affichage stable ;
- pas de hiérarchie, pas de couleur, pas de type de tag au MVP ;
- capacité à lister les tags existants par bibliothèque ou globalement pour les suggestions/filtres ;
- suppression d'un tag d'un objet sans supprimer les autres occurrences.
Point à trancher : stockage en table de jointure normalisée ou liste normalisée par entité. UX ne dépend pas du choix, tant que les filtres et suggestions restent rapides en local.
### Duplication programme
Use case attendu : `duplicateProgram(programId)` ou équivalent, qui crée une copie profonde :
- nouveau `Program.id` et nouvelles métadonnées ;
- nom généré sans conflit ;
- `isExample = false` ;
- exercices du programme copiés en nouvelles lignes configurées ;
- snapshots, mesures, cibles, repos, étapes et réglages copiés ;
- tags copiés ;
- aucun lien fonctionnel vers l'original.
### Duplication séance-modèle
Use case attendu : `duplicateWorkoutTemplate(templateId)` ou équivalent, qui crée une copie profonde :
- nouveau `WorkoutTemplate.id` et nouvelles métadonnées ;
- nom généré sans conflit ;
- `isExample = false` ;
- `lastStartedAt = null` ;
- programmes intégrés copiés avec leurs snapshots ;
- overrides copiés et reliés aux nouveaux programmes copiés ;
- tags copiés ;
- aucun lien fonctionnel vers l'original.
## Étape suivante
Architect doit cadrer le modèle de données des tags et les use cases de duplication profonde avant implémentation DevBackend/DevFrontend.
# Architect — Cadrage #83
## Décision modèle tags
Retenir un stockage **dénormalisé par agrégat**, via une colonne JSON sur chaque ressource taggable : `exercises.tags_json`, `programs.tags_json`, `workout_templates.tags_json`.
Ne pas créer de tables `tags` / `taggings` pour le MVP.
Raisons :
- Le besoin UX exclut hiérarchie, couleurs, écran global et suppression globale de tag. Un tag n'a donc pas d'identité métier propre : c'est un libellé attaché à un objet.
- Le serveur sync v1 stocke des payloads JSON opaques par ressource (`exercise`, `program`, `workoutTemplate`). Ajouter `tags` au payload de la ressource est naturel ; créer une ressource syncable de jointure introduirait du LWW et des conflits pour une donnée simple.
- Les bibliothèques restent locales et de taille raisonnable ; le filtrage peut être fait après `listActive()` sans index JSON. Si un jour les bibliothèques deviennent volumineuses, on pourra normaliser en lecture sans changer l'UX.
- L'ordre d'affichage stable est plus simple à garantir avec une liste ordonnée portée par l'entité.
## Contrat tag dans le domaine
Ajouter `List<String> tags = const []` sur :
- `Exercise`
- `Program`
- `WorkoutTemplate`
Invariants domaine :
- normaliser à l'entrée : `trim`, collapse des espaces internes, `toLowerCase()` ; la casse saisie n'est pas conservée au MVP, conformément à la recommandation UX de tags visuellement en minuscules ;
- supprimer les tags vides après normalisation ;
- maximum 8 tags par objet ;
- maximum 24 caractères par tag normalisé ;
- doublons interdits sur un même objet après normalisation ;
- ordre conservé selon l'ordre de la liste fournie après déduplication/validation.
Types/erreurs recommandés : pas besoin d'une entité `Tag`. Utiliser les invariants des constructeurs/copyWith et lever `DomainException` avec messages mappables par l'UI : tag vide, tag trop long, trop de tags, doublon.
## Drift et migration
Le schéma local est actuellement `schemaVersion = 18` (`lib/infrastructure/local/app_database.dart:51`). #83 doit passer en **19**.
Dans `lib/infrastructure/local/tables.dart`, ajouter :
```dart
TextColumn get tagsJson => text().withDefault(const Constant('[]'))();
```
sur `Exercises`, `Programs`, `WorkoutTemplates`. Si l'équipe préfère contraindre SQLite, ajouter une contrainte table `CHECK (json_valid(tags_json))`; le projet utilise déjà JSON1 dans des migrations, donc c'est acceptable.
Migration `AppDatabase._migrateToSchema19()` :
```dart
Future<void> _migrateToSchema19() async {
await _addColumnIfMissing(
tableName: 'exercises',
columnName: 'tags_json',
definition: "tags_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags_json))",
);
await _addColumnIfMissing(
tableName: 'programs',
columnName: 'tags_json',
definition: "tags_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags_json))",
);
await _addColumnIfMissing(
tableName: 'workout_templates',
columnName: 'tags_json',
definition: "tags_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags_json))",
);
}
```
Ajouter `if (from < 19) await _migrateToSchema19();` dans `onUpgrade`. `onCreate` passera par `migrator.createAll()` avec les nouvelles colonnes, pas besoin de traitement spécifique hors maintien des migrations existantes.
Pas d'index JSON au MVP. Les index existants `_createIndexes()` couvrent déjà `deleted_at`, `updated_at`, `sync_state`, `local_revision` pour les tables syncables. Les filtres tags sont faits côté application/présentation sur les listes actives chargées.
## Mapping local et payload sync
Dans `drift_repositories.dart` :
- encoder/décoder `tagsJson` avec helpers dédiés (`_encodeTags`, `_decodeTags`) qui tolèrent JSON absent/malformé en fallback `[]` côté payload distant ancien ;
- mapper `tags` dans `_exerciseCompanion`, `_programCompanion`, `_workoutTemplateCompanion` ;
- mapper `tags` dans `_exerciseFromRow`, `_programFromRow`, `_workoutTemplateFromRow` ;
- ajouter `tags` aux payloads `_exercisePayload`, `_programPayload`, `_workoutTemplatePayload` ;
- lire `tags` dans `_exerciseFromPayload`, `_programFromPayload`, `_workoutTemplateFromPayload`, avec défaut `[]` pour compatibilité des anciens payloads.
Impact sync : aucun changement serveur. Les tags voyagent dans le payload JSON opaque de `exercise`, `program`, `workoutTemplate`. Une modification de tags doit toucher `metadata.updatedAt`, incrémenter `localRevision`, marquer `syncState = dirty` via les saves existants, et donc remonter si la sync #63-70 est active. Les copies créées localement sont de nouvelles ressources normales, avec nouveaux IDs locaux ; elles seront poussées comme `program` ou `workoutTemplate` nouveaux. Pas de migration PostgreSQL/OpenAPI.
## API application tags
Étendre les signatures existantes sans créer de port séparé :
- `ExerciseUseCases.create/update(..., List<String> tags = const [])`
- `ProgramUseCases.create/saveConfigured(..., List<String> tags = const [])`
- `WorkoutTemplateUseCases.create/saveConfigured(..., List<String> tags = const [])`
Les repositories restent `findById/listActive/save`; pas besoin de méthodes SQL de filtre au MVP.
Ajouter un petit service/helper pur côté application pour la logique de filtre/suggestions, ou des méthodes sur les use cases :
```dart
List<T> filterByRequiredTags<T>(List<T> items, Set<String> requiredTags);
List<TagUsage> tagSuggestionsFor<T>(List<T> items);
```
Règles : normaliser les tags sélectionnés avec la même fonction que le domaine ; un objet matche s'il contient **tous** les tags sélectionnés ; suggestions triées par usage décroissant puis alphabétique.
## Duplication programme
Ajouter dans `ProgramUseCases` :
```dart
Future<Program> duplicate(String id)
```
ou `duplicateProgram(String programId)` selon le style retenu par DevBackend.
Contrat :
1. Lire le programme source via `programRepository.findById(id)`, sinon `DomainException('Program not found.')`.
2. Lire les programmes actifs via `programRepository.listActive()` pour générer un nom sans conflit.
3. Nom : `Copie de X`; si déjà pris, `Copie 2 de X`, `Copie 3 de X`, etc. Comparaison de conflit sur `trim().toLowerCase()` parmi les programmes actifs non supprimés.
4. Créer un nouveau `Program` avec nouveau `metadata.id`, `createdAt = updatedAt = now`, `originDeviceId` courant, `isExample = false`, `tags = source.tags`.
5. Copier chaque `ProgramExercise` en nouvelle entité enfant : nouveau `metadata.id`, `programId = newProgramId`, `position` identique, tous les snapshots/mesures/cibles/repos/steps/autoStart copiés tels quels. `sourceExerciseId` reste copié : c'est le lien normal vers l'exercice source, pas un lien vers le programme original.
6. Sauvegarder via `programRepository.replaceExercises(copy, now)` ou une transaction équivalente.
7. Retourner la copie complète pour ouverture directe en édition.
Ne pas ajouter `duplicatedFromId`. Choix explicite : la copie est un nouvel agrégat utilisateur autonome ; aucune UX/debug/sync actuelle ne consomme une traçabilité d'origine. Ajouter ce champ maintenant créerait une relation inutile et une question de sync/LWW sans valeur MVP.
## Duplication séance-modèle
Ajouter dans `WorkoutTemplateUseCases` :
```dart
Future<WorkoutTemplate> duplicate(String id)
```
ou `duplicateWorkoutTemplate(String templateId)`.
Contrat :
1. Lire la séance source via `templateRepository.findById(id)`, sinon `DomainException('Workout template not found.')`.
2. Lire les séances actives via `templateRepository.listActive()` pour générer le nom sans conflit (`Copie de X`, puis `Copie 2 de X`, etc.).
3. Créer un nouveau `WorkoutTemplate` avec nouveau `metadata.id`, `lastStartedAt = null`, `isExample = false`, `tags = source.tags`.
4. Copier chaque `WorkoutTemplateProgram` avec nouveau `metadata.id`, `workoutTemplateId = newTemplateId`, `position` identique, `programNameSnapshot`, `defaultRestSecondsSnapshot`, `programSnapshotJson` copiés tels quels. `sourceProgramId` peut rester copié : c'est le lien optionnel vers le programme bibliothèque source, pas vers la séance originale.
5. Construire une map `oldWorkoutTemplateProgramId -> newWorkoutTemplateProgramId`.
6. Copier chaque `WorkoutTemplateExerciseOverride` avec nouveau `metadata.id`, `workoutTemplateProgramId` remappé vers le nouveau programme copié, `snapshotProgramExerciseId` inchangé, tous les overrides numériques/autoStart copiés.
7. Sauvegarder via `templateRepository.replaceComposition(copy, now)`.
8. Retourner la copie complète pour ouverture directe en édition.
Aucun lien `duplicatedFromId` pour les mêmes raisons que les programmes.
## Lots backend/frontend
### B1 — Tags : modèle, CRUD, filtrage
- Bump Drift `schemaVersion` 18 -> 19.
- Ajouter `tags_json` sur `exercises`, `programs`, `workout_templates` + migration idempotente.
- Ajouter `tags` aux entités domaine et aux `copyWith`.
- Ajouter validation/normalisation domaine.
- Mapper Drift row/companion/payload/pull payload.
- Étendre use cases create/update/saveConfigured avec `tags`.
- Ajouter helper/application pour suggestions et filtrage multi-tags.
- Tests domaine : normalisation, doublons, limite 8, limite 24.
- Tests infra : migration colonnes, save/load tags, payload sync avec tags, payload ancien sans tags -> `[]`.
- Tests application : suggestions triées usage desc puis alpha, filtre AND.
### B2 — Duplication programme
- Ajouter `ProgramUseCases.duplicate`.
- Copier `Program` + tous `ProgramExercise` en nouveaux IDs.
- Générer nom sans conflit.
- Forcer `isExample = false`, copier `tags`, ne pas ajouter de lien d'origine.
- Tests application : copie profonde, conflit de nom, tags copiés, exemple non copié, modification source/copie indépendante.
- Tests infra si nécessaire : persistance enfants copiés et change log des nouvelles entités.
### B3 — Duplication séance-modèle
- Ajouter `WorkoutTemplateUseCases.duplicate`.
- Copier `WorkoutTemplate` + `WorkoutTemplateProgram` + overrides avec remap des IDs programme.
- Générer nom sans conflit.
- Forcer `isExample = false`, `lastStartedAt = null`, copier `tags`, pas de lien d'origine.
- Tests application : copie profonde, overrides remappés, conflit de nom, tags copiés, lastStartedAt vidé, exemple non copié.
### F1 — Tags UI et filtres
- Ajouter section Tags aux formulaires exercice/programme/séance.
- Afficher badges tags dans les listes selon les limites UX (`max 3`, puis `+N`).
- Ajouter recherche aux programmes/séances.
- Ajouter chips de filtres tags, logique AND, suggestions triées, action `Effacer les filtres`, bottom sheet `Voir tous les tags` si > 12.
- Conserver catégorie exercice et filtres mesures existants.
- Tests widget : ajout/suppression tag, validation, filtres vides/non vides, badges compactés.
### F2 — Duplication UI
- Remplacer suppression visible des programmes par menu `...` : `Dupliquer`, `Supprimer`.
- Séances : conserver `Lancer`, ajouter menu `...` avec `Dupliquer`, `Supprimer`.
- Appeler les use cases de duplication, désactiver l'action pendant l'opération si nécessaire.
- Ouvrir directement l'écran d'édition de la copie.
- Snackbars exactes UX.
- Tests widget : menu présent, duplication programme ouvre édition, duplication séance ouvre édition, erreur affiche `Duplication impossible pour le moment.`
## Étape suivante
Git doit créer la branche #83. Ensuite DevBackend peut prendre B1 -> B2 -> B3 ; DevFrontend peut démarrer F1 après B1 et F2 après B2/B3.
# DevBackend — Implémentation B1/B2/B3 (commit add5025)
## Réalisé
- B1 : `tags_json` ajouté sur `exercises`/`programs`/`workout_templates`, migration Drift schemaVersion 18 → 19, normalisation domaine (minuscules, espaces collapsés, max 8 tags, max 24 caractères, doublons refusés), mapping payload sync avec fallback `[]` pour anciens payloads.
- B1 : helpers applicatifs filtre AND multi-tags + suggestions triées par usage décroissant puis alphabétique.
- B2 : `ProgramUseCases.duplicate` — copie profonde avec nouveaux IDs, nom `Copie de X` / `Copie 2 de X` en cas de conflit, tags copiés, `isExample = false`, pas de `duplicatedFromId`.
- B3 : `WorkoutTemplateUseCases.duplicate` — copie profonde avec remap des IDs de programmes intégrés et overrides, nom sans conflit, tags copiés, `isExample = false`, `lastStartedAt = null`.
## Fichiers touchés
- `lib/application/ports.dart`
- `lib/application/use_cases.dart`
- `lib/domain/entities.dart`
- `lib/infrastructure/local/app_database.dart`
- `lib/infrastructure/local/app_database.g.dart`
- `lib/infrastructure/local/drift_repositories.dart`
- `lib/infrastructure/local/tables.dart`
- `test/application/use_cases_test.dart`
- `test/infrastructure/drift_repositories_test.dart`
## Vérification Main (commit add5025)
`flutter test --no-pub test/application/use_cases_test.dart test/infrastructure/drift_repositories_test.dart` exécuté en environnement fonctionnel : 110 tests, 1 seul échec — dette préexistante FK media déjà documentée (#80/#81/#82), sans rapport avec #83. `dart analyze` propre.
**Lot backend B1/B2/B3 : GO.**
Étape suivante : DevFrontend enchaîne F1 (tags UI + filtres) puis F2 (duplication UI, menu `...`).

View File

@ -10,8 +10,8 @@ agentRefs: []
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784650720801
updatedAt: 1784650720801
version: 1
updatedAt: 1784707484092
version: 5
---
Constat Commercial : les apps concurrentes permettent de dupliquer/adapter rapidement un programme existant plutôt que de le recréer, et d'organiser une bibliothèque grandissante par tags (compétence, matériel, durée, intensité).

View File

@ -941,7 +941,7 @@
"priority": "medium",
"sprint": null,
"assignedAgentIds": [],
"updatedAt": 1784650720801
"updatedAt": 1784707484092
},
{
"issueRef": "#84",