Bikefit / Positionnement — Proposition d’architecture (fiches, vélos, historique)¶
Problème constaté¶
Aujourd’hui, on essaye de tout faire dans un seul formulaire (créer une fiche, filtrer par vélo, pré-remplir depuis une autre fiche, choisir une “version actuelle”, etc.).
Ça crée :
- Une UI fragile sur mobile (sélecteurs/pills qui débordent)
- Une confusion métier : “actuelle… pour quoi ? pour quel vélo ?”
- Une difficulté à gérer la réalité suivante : une mesure d’un vélo peut servir de base pour un autre vélo, mais la nouvelle mesure a sa propre vie (liée au nouveau vélo).
Objectifs produit¶
- Le Bikefit est une suite de fiches (snapshots), chacune liée à un vélo cible.
- Une fiche peut être faite par un pro (version “pro”).
- Une fiche peut être dérivée d’une autre fiche (même vélo ou vélo différent) pour servir de base.
- La navigation doit être simple :
- “Je choisis un vélo → je vois son historique → j’ajoute / je base sur…”
- Ajouter un champ date (date de la mesure) pour journaliser l’ordre des changements, indépendamment de
created_at.
Concepts clés (glossaire)¶
- Vélo cible : le vélo auquel la fiche appartient.
- Fiche (snapshot) : une photo des mesures à une date donnée, avec notes.
- Pro : une fiche réalisée par un professionnel.
- Actuelle (pour un vélo) : la fiche de référence pour ce vélo-là.
- Base / source : une fiche existante (potentiellement d’un autre vélo) utilisée pour pré-remplir une nouvelle fiche.
Règles métier proposées¶
- Une fiche appartient à un seul vélo cible.
- “Actuelle” est une propriété par vélo (ex: une seule fiche “actuelle” pour le vélo A, une autre pour le vélo B).
- Règle UX : on vise exactement 1 actuelle par vélo.
- Fallback : si aucune n’est marquée (cas legacy / edge), l’app considère la plus récente comme “actuelle”.
- Une fiche peut être créée “basée sur” une autre fiche (source). Dans ce cas :
- La nouvelle fiche reste liée au vélo cible
- On garde un lien
derived_from_snapshot_id - Règle produit (soft) : une fiche marquée “pro” devrait en général être une fiche faite “from scratch”. Si l’utilisateur coche “Pro” alors que la fiche est dérivée, on ne bloque pas, mais on affiche une confirmation.
- On introduit
measured_at(date de la mesure, saisie/éditable) en plus decreated_at(audit technique). - Décision : date seulement (pas d’heure). Par défaut : date du jour.
Variante optionnelle (si besoin plus tard) : distinguer “actuelle pour ce vélo” vs “template/baseline recommandé” avec un champ
is_template(ou une table dédiée). Ça évite de tordre le sens de “actuelle”.
Modèle de données (proposition)¶
Nom : la table existe déjà : public.fit_snapshots (Supabase).
Champs suggérés :
id(uuid)user_idbike_id(vélo cible)created_at(timestamp, auto)
Champs existants (au moment d’écrire ce doc) :
name(text)- mesures en colonnes (ex:
saddle_height_mm,stem_length_mm, etc.) other_notes(text)is_professional(bool)is_current(bool) — c’est la “current pour ce vélo”
Champs à ajouter :
measured_at(date, éditable)derived_from_snapshot_id(uuid, nullable)
Contraintes DB (Postgres) :
- Une seule “current” par vélo : index unique partiel sur
(bike_id)oùis_current = true(déjà en place).
Pas de contrainte DB pour “pro + dérivée” (support) :
- On laisse la DB accepter les cas ambigus
- On gère ça dans l’UI avec une confirmation explicite
Texte de confirmation (exemple, à affiner) :
Vous êtes sur le point de marquer cette fiche comme “faite par un pro”, mais elle a été créée à partir d’une mesure existante. Voulez-vous continuer ?
UX / Navigation (proposition)¶
1) Sortir le “choix du vélo” du formulaire¶
- La sélection de vélo sert à naviguer dans le service.
- Donc : un sélecteur de vélo au niveau de la page (header de
/dashboard/bikefit) et pas dans la modal.
2) Page “hub” Bikefit¶
/dashboard/bikefit
- En haut : Select plein largeur “Vélo” (mobile-safe)
- Ensuite :
- Bloc “Fiche actuelle” (si existe)
- Timeline / liste des fiches (tri par
measured_atdesc, fallbackcreated_at) - Filtres simples : Pro / Perso / Dérivées
Actions :
Ici, “vélo cible fixé” veut dire : si l’utilisateur a déjà choisi un vélo dans le header du hub, ce vélo devient le contexte courant.
- Nouvelle fiche (vierge) → lance le wizard en sautant l’étape “choisir le vélo cible” (puisqu’on le connaît déjà) et ouvre directement le formulaire.
- Nouvelle fiche basée sur… → mini-wizard :
- Choisir une fiche source (recherche + Select/Combobox, pas de pills)
- Choisir le vélo cible (par défaut = vélo sélectionné sur le hub, mais modifiable)
- Formulaire de saisie
2.1) Wizard — écrans & micro-copy (proposition)¶
But : rendre la création “claire” même avec la complexité cross-vélo.
Entrées possibles :
- Depuis le hub (principal)
- Depuis une fiche (raccourci : “Utiliser comme base…”) — pré-remplit la source
Écran 0 — Type de création
- Choix :
- “Nouvelle fiche (vierge)”
- “Nouvelle fiche basée sur une fiche existante”
Écran 1 — Choisir une fiche source (si “basée sur”)
- Recherche + liste (afficher : vélo, nom, date
measured_at, badge Pro / Current) - Micro-copy :
- “Choisissez une fiche comme point de départ. Vous pourrez ensuite la modifier pour le vélo cible.”
Décision :
- Cas fréquent = révision sur le même vélo : par défaut, proposer en premier la fiche actuelle du vélo.
- On permet quand même de sonder les autres fiches (y compris d’autres vélos) via la recherche.
Écran 2 — Vélo cible
- Select “Vélo cible” (par défaut : vélo déjà sélectionné sur le hub)
- Micro-copy (si source d’un autre vélo) :
- “La fiche sera créée pour le vélo sélectionné. La source sera conservée dans l’historique.”
Écran 3 — Détails & mesures
- Champs minimum (mode simple) :
- Nom de la fiche (suggestion auto)
- Date de mesure (
measured_at) (par défaut : aujourd’hui) - Mesures
- Notes
- Contexte / raison de l’ajustement (champ séparé)
-
“Définir comme actuelle pour ce vélo” (
is_current) -
Option avancée (si activée) :
- Toggle “Fait par un pro” (
is_professional)
Confirmation “Pro + dérivée” (soft rule)
Déclenchement : is_professional = true ET derived_from_snapshot_id != null.
Texte proposé :
Cette fiche est marquée “faite par un pro”, mais elle a été créée à partir d’une fiche existante. Voulez-vous continuer ?
Actions : “Continuer” / “Annuler” (ou “Continuer” / “Retirer le badge pro”).
Règle de copie (basée sur une source) :
- Copier : les mesures numériques + notes “techniques” si pertinent
- Ne pas copier : le champ contexte / raison de l’ajustement (doit être rempli spécifiquement pour cette nouvelle fiche)
Traçabilité :
- On conserve
derived_from_snapshot_iden BD - Pas besoin de l’afficher dans l’UI pour l’instant
3) Page détail d’une fiche¶
Dans une fiche :
- Bouton “Définir comme actuelle pour ce vélo”
- Bouton “Utiliser comme base pour…” → demande le vélo cible puis ouvre la création
Note découverte UX : pour éviter que l’utilisateur doive “deviner” qu’il faut ouvrir une fiche d’un autre vélo pour s’en servir comme base, le hub doit aussi proposer “Nouvelle fiche basée sur…” avec une liste de sources qui peut venir de tous les vélos. Le bouton dans la fiche devient alors un raccourci, pas le seul chemin.
Scénarios supportés (ce que ça résout)¶
- Bikefit pro pour un vélo : une fiche pro, liée au vélo, datée, et on peut la marquer “actuelle pour ce vélo”.
- Utiliser une fiche d’un autre vélo comme base : création d’une nouvelle fiche sur le nouveau vélo + lien vers la source + date.
- Historiser l’ordre :
measured_atpermet de reconstruire une timeline réelle (même si la saisie est faite plus tard).
Questions à trancher (rapides)¶
- “Pro” : est-ce qu’on veut tracer qui est le pro (nom, user, atelier) ?
- Option A (simple) : seulement un toggle “Pro”
- Option B (avancé) :
fitter_name/fitter_id/ atelier - Recommendation : attendre la demande et garder ça derrière un mode/affichage avancé
- Est-ce qu’une fiche peut avoir plusieurs sources (rare) ? Si non,
derived_from_snapshot_idsuffit. - Est-ce qu’on a besoin d’un concept “template/baseline recommandé” séparé de “actuelle” ? (probablement oui à moyen terme)
Mode simple vs avancé (proposition)¶
Pour éviter d’exploser la complexité pour tout le monde :
- Mode simple (amateur sérieux) :
- vélo cible,
measured_at, notes - marquer “actuelle pour ce vélo”
- “basée sur…” (source) sans options avancées
- Mode avancé :
- toggle “Pro” + éventuellement identité du fitter
- plus de métadonnées (si besoin)
- actions/outils supplémentaires (ex: tags, contextes d’usage)
Baseline vs “actuelle”¶
Si on veut garder les choses simples :
- La baseline peut être implicitement “la dernière fiche pro pour ce vélo” si elle existe.
- “Actuelle” reste la fiche choisie comme référence opérationnelle pour ce vélo.
Décision actuelle :
- Dans l’app, c’est l’actuelle qui doit être affichée en premier et utilisée “par défaut” pour le Bikefit.
- Le rôle exact de “baseline pro” reste à préciser selon les écrans/outils (à rediscuter quand le besoin est clair).
On peut afficher les deux sections :
- Baseline (Pro) — dernière fiche pro (si elle existe)
- Actuelle — fiche marquée current (sinon fallback sur la plus récente)
À garder en tête (futur : Événements)¶
Un futur service “Événements” voudra associer, pour un événement donné :
- un vélo
- un bikefit (snapshot)
- une pression (et potentiellement d’autres paramètres)
Implications pour la BD :
- éviter de supprimer/écraser des snapshots : préférer l’historique (IDs stables)
measured_atdevient très utile pour reconstruire la chronologie- prévoir que d’autres tables référenceront
bikefit_snapshots.id(foreign keys / soft-delete)
Migration BD (Supabase) — proposition compatible¶
Objectif : ajouter measured_at + derived_from_snapshot_id sans casser l’existant.
1) Ajouter measured_at (nullable) puis backfill created_at, puis NOT NULL + default current_date
2) Ajouter derived_from_snapshot_id avec FK on delete set null
3) (Optionnel) Ajouter un index pour filtrer rapidement les dérivées + trier par date
Note : pas de contrainte DB qui bloque “pro + dérivée”. On s’appuie sur la confirmation UI.
Suppression & cohérence “current”¶
Décision : supprimer une fiche “current” est permis, mais on doit préserver la règle “1 current par vélo”.
- Si on supprime la fiche
is_current = true, l’app (ou une opération DB dédiée) doit promouvoir automatiquement la fiche la plus récente du même vélo enis_current = true.
Implementation recommandée :
- Trigger DB
AFTER DELETEsurfit_snapshotsqui, si un vélo n’a plus aucune ficheis_current = true, promeut la plus récente.
Note sur measured_at¶
Si measured_at a été ajouté en timestamptz par une première migration, il est normal que tu aies l’impression que “c’était déjà ça”.
La décision finale ici est date-only : on convertit donc measured_at en date et on met le default à current_date.