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:
@ -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 `...`).
|
||||
@ -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é).
|
||||
|
||||
|
||||
@ -941,7 +941,7 @@
|
||||
"priority": "medium",
|
||||
"sprint": null,
|
||||
"assignedAgentIds": [],
|
||||
"updatedAt": 1784650720801
|
||||
"updatedAt": 1784707484092
|
||||
},
|
||||
{
|
||||
"issueRef": "#84",
|
||||
|
||||
Reference in New Issue
Block a user