diff --git a/.ideai/tickets/83/carnet.md b/.ideai/tickets/83/carnet.md index 68e2872..57d6b24 100644 --- a/.ideai/tickets/83/carnet.md +++ b/.ideai/tickets/83/carnet.md @@ -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 ` ; +- si ce nom existe déjà, utiliser `Copie 2 de `, puis `Copie 3 de ` ; +- 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 ` ; +- si conflit : `Copie 2 de `, `Copie 3 de ` ; +- 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 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 _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 tags = const [])` +- `ProgramUseCases.create/saveConfigured(..., List tags = const [])` +- `WorkoutTemplateUseCases.create/saveConfigured(..., List 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 filterByRequiredTags(List items, Set requiredTags); +List tagSuggestionsFor(List 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 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 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 `...`). \ No newline at end of file diff --git a/.ideai/tickets/83/issue.md b/.ideai/tickets/83/issue.md index 8a64ce7..3340b4d 100644 --- a/.ideai/tickets/83/issue.md +++ b/.ideai/tickets/83/issue.md @@ -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é). diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index a0d9d50..bfe12ec 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -941,7 +941,7 @@ "priority": "medium", "sprint": null, "assignedAgentIds": [], - "updatedAt": 1784650720801 + "updatedAt": 1784707484092 }, { "issueRef": "#84",