Chaque page reprend le socle du lot 2 : en-tête avec une phrase
d'explication, couleurs du thème (plus de pages figées en blanc), textes
de 12 px minimum, noms de magasins, infobulles sur le jargon, retours par
toast / confirmation, états vides et d'erreur expliqués.
- Accueil : CA, tickets et panier moyen d'hier vs N-1, par magasin, top 10
avec lien vers la fiche produit.
- Révision d'assortiment : un seul endroit pour enregistrer et exporter
(menu Export unique, mêmes droits pour tous), annulation confirmée, sens
des gammes affiché partout, magasin choisi en un clic, colonnes nommées
en clair, bandeau de progression au lieu de la fenêtre floue, droits lus
côté serveur, ligne sélectionnée opaque sur les colonnes figées.
- Recherche produit, Stocks à surveiller (export FF inchangé), Meilleures
ventes, Ventes par mois (dernier mois complet par défaut), Publicités,
Commandes fournisseurs, Historique, Paramètres (onglets), Synchronisation
et Connexion migrés sur DataTable / Tabs / Card.
- Le nom de l'utilisateur est transmis à la session (affiché dans
l'en-tête).
- Composants devenus inutiles supprimés (anciennes fenêtres modales,
export-dropdown, financial-cell, supplier-selection).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
Thème
- Clair par défaut ; les classes dark: suivent le bouton de thème (et non
plus le réglage du système) ; animations tw-animate-css enfin importées.
- Couleurs shadcn branchées sur les variables (survols des menus visibles),
libellés gris au contraste AA dans les deux thèmes, fond plein pour le
menu et l'en-tête.
Composants partagés (src/components/ui)
- PageHeader, Card/Field, StatCard, Badge/DeltaBadge/StoreBadge/GammeBadge,
Tabs/Segmented/useUrlTab, Select/SearchInput, EmptyState/ErrorState/
Skeleton, Tooltip/Terme (glossaire), toast et confirmer, Pagination,
DataTable (recherche, filtres, tri, pagination, export, totaux).
- Sources uniques : magasins, gammes (avec leur sens), glossaire, seuils de
marge, export Excel.
Navigation
- Menu groupé (Au quotidien / Analyses / Suivi / Administration), déplié
par défaut, libellés en français.
- En-tête : nom de la page, thème, menu du compte avec le vrai nom, la
hauteur des lignes de la Grille et la déconnexion. La recherche globale,
qui ne filtrait que la Grille, est retirée.
- Page Historique (sessions + exports) ; /snapshots et /exports y
redirigent.
- Squelettes de chargement au lieu de la fenêtre plein écran, pages
d'erreur et « introuvable » en français, arrivée sur l'Accueil après
connexion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
C'est le bon indicateur, et le volume brut ne pouvait pas le remplacer :
il confond la performance du produit et sa diffusion. Un produit monte en
volume des qu'on le reference ailleurs, sans mieux se vendre nulle part ;
un produit retire de 60 magasins s'effondre en volume tout en performant
mieux la ou il reste. Seule la qte/magasin repond a « ce produit est-il
tendance ».
- computeNetworkTrend prend le nombre de magasins par mois et calcule
l'evolution sur qte/magasins vendeurs. Sans cette serie, repli sur le
volume brut et `surQteParMagasin` a false, pour que l'UI le dise.
- La serie remonte jusqu'a tous les appelants : grille, fiche produit et
recherche produits (nbMagByMonth ajoute au resultat de recherche).
- La carte affiche desormais trois bandes empilees sur le meme axe :
qte/magasin en PRINCIPALE (couleur de la tendance, aire pleine),
quantites vendues et magasins vendeurs en contexte. La teinte
« tendance » ne designe plus qu'une seule chose dans le graphique ; les
bandes de contexte ont des couleurs fixes.
- Le pourcentage de la carte precise « (qte/magasin) » : un chiffre de
tendance sans son assiette se prete a tous les malentendus.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Trois defauts signales, trois causes distinctes.
1. LA COURBE DES MAGASINS N'APPARAISSAIT PRESQUE JAMAIS.
L'extracteur ecrivait `{ qte: 0 }` sur les mois sans vente, sans
`nbMag`. La serie exigeant les 12 mois, elle disparaissait des qu'un
produit avait un mois creux — c'est-a-dire presque toujours. Un mois
PRESENT dans le detail est un mois extrait : s'il n'a pas de nbMag,
c'est que personne n'a vendu, donc zero. Corrige a la source ET a la
lecture, pour que les caches deja ecrits repartent sans re-sync.
2. LES DEUX COURBES ETAIENT ILLISIBLES SUPERPOSEES.
Deux echelles independantes placaient la courbe des magasins AU-DESSUS
de celle des quantites : cela se lit spontanement comme « il y a plus
de magasins que de ventes », un contresens. Les series sont desormais
tracees dans DEUX BANDES distinctes alignees sur le meme axe des mois.
On compare la forme de l'une avec la forme de l'autre, sans jamais
suggerer un rapport de grandeur.
3. LES POURCENTAGES N'AVAIENT PAS DE SENS.
La pente d'une regression rapportee a la moyenne donnait « -124 % »
(une baisse ne peut pas depasser -100 %), « +1 062 % » pour 2 unites
vendues une fois dans l'annee, et surtout « +508 % » sur des dizaines
de lignes d'affilee — la signature arithmetique d'une serie nulle
partout sauf le dernier mois, ou le rapport ne depend meme plus des
quantites. On compare maintenant deux moyennes de 4 mois : variation
qui se lit comme telle, plancher a -100 %, insensible au bruit d'un
mois isole. Sans base de comparaison, aucun pourcentage n'est
invente — le produit est marque « Nouveau », et le tri le remonte en
tete au lieu de l'enterrer.
Le graphique accepte aussi des quantites negatives (retours superieurs
aux ventes sur un mois) : l'echelle part du minimum reel, le point ne
sort plus du cadre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Une quantite qui monte parce que le produit est diffuse dans plus de
magasins ne raconte pas la meme histoire qu'une quantite qui monte a
diffusion constante. La courbe des quantites seule ne permettait pas de
distinguer un succes produit d'un simple elargissement.
- computeStoresSeries() : serie « magasins vendeurs » sur la MEME fenetre
12 mois que la tendance, avec la meme regle du tout ou rien — les 12
cles doivent etre presentes, sinon null. Un mois non extrait ne doit
pas se lire comme « zero magasin », ce qui simulerait un arret de
diffusion.
- nbMagReseauByMonth remonte du cache (metricsByMonth, deja stocke par la
sync) jusqu'a la ligne de grille.
- NetworkLineChart accepte une serie `stores` optionnelle.
DEUX ECHELLES INDEPENDANTES : les quantites se comptent en milliers, les
magasins plafonnent a ~270. Sur une echelle commune la courbe des
magasins serait ecrasee sur l'axe et ne montrerait rien. Chaque serie est
normalisee sur son propre maximum, rappele dans la legende — c'est la
FORME des deux courbes qu'on compare, pas leurs hauteurs. La courbe des
magasins est pointillee et indigo, pour ne jamais entrer en collision
avec le vert/rouge/gris de la tendance.
Branchee sur la carte de la Grille et sur celle de la fiche produit. La
sparkline de la colonne reste a une seule courbe : 46x18 px, deux traces
y seraient illisibles.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
« Unexpected token '<', "<html> <h"... is not valid JSON » : ce n'était pas du
JSON parce que ce n'était pas l'application qui répondait. La recherche enchaîne
deux allers-retours Qlik (dont une extraction mensuelle) et dépassait le délai du
proxy, qui renvoyait sa page d'erreur HTML.
- L'API passe en asynchrone, sur le modèle déjà éprouvé de POST /api/qlik/sync :
POST démarre un job et rend la main immédiatement, GET renvoie l'avancement
puis le résultat. Le client interroge toutes les 2 s et affiche l'étape en
cours (recherche des articles / extraction des ventes / rapprochement
catalogue). Deux recherches simultanées au maximum.
- Le client ne présume plus que la réponse est du JSON : les statuts 504, 502,
503, 401 et 403 sont traduits en message actionnable au lieu d'une erreur de
parsing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Symptôme : « tapis anti » renvoyait 40 articles sans rapport (mugs, miroirs,
couvertures) avec des codes centraux contigus — c'est-à-dire le début du
catalogue Qlik, pas un résultat de recherche.
Cause : SearchResults + SelectValues. On ré-injectait les qText renvoyés par la
recherche globale dans un SelectValues sur le champ, sans vérifier le retour et
sans laisser à l'Engine le temps d'évaluer la sélection. Quand la sélection
n'est pas appliquée, le cube renvoie simplement les premières lignes de la
dimension « Article Code » — soit exactement les 40 lignes demandées.
Correctifs :
- Sélection via list object : SearchListObjectFor + AcceptListObjectSearch,
c'est-à-dire le mécanisme d'un volet de filtre Qlik. Qlik applique sa propre
sémantique de recherche et sélectionne lui-même les valeurs, sans réinjection
de valeurs texte. Libellé d'abord, code centrale en repli.
- Délai d'évaluation (QLIK_SETTLE_MS, défaut 250 ms) après ClearAll, après la
recherche et après l'acceptation, comme le fait déjà l'extraction.
- Garde-fou côté client : chaque ligne du cube est revérifiée contre le terme
(tous les mots présents dans le libellé, insensible casse/accents, ou code
correspondant). Une liste sans rapport avec la recherche est pire que zéro
résultat.
- Dédoublonnage par code centrale : un article référencé chez plusieurs
fournisseurs produisait autant de lignes (vu sur « Couverture de Pique-nique »,
code 10636135, listé deux fois).
- Diagnostic : inventaire des champs, nombre de valeurs trouvées, sélections
actives et compteur de lignes écartées sont tracés dans les logs ; si tout est
écarté, l'UI dit que le filtre n'a pas été appliqué au lieu d'afficher un
« aucun résultat » trompeur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Recherche produit
- La page /produits interroge désormais Qlik Sense EN PREMIER
(src/lib/qlik-search.ts) : recherche globale Engine sur les champs article
(code + libellé détectés dynamiquement via FieldList), sélection des valeurs
trouvées, puis liste des Article Code correspondants.
- Les métriques réseau des articles trouvés sont extraites sur 12 mois
glissants et mises en cache (qlik_network_metrics), puis rapprochées du
catalogue FF Nancy par code centrale (pgGetProduitsByCodeCentrale).
- Repli propre sur la recherche catalogue local si Qlik est injoignable, signalé
dans l'UI. Résultats mis en cache mémoire 10 min, requête dédupliquée.
- Nouvelle route GET /api/produits/search (runtime nodejs, 300 s) ; la liste est
chargée côté client avec état de chargement, l'extraction Qlik prenant
plusieurs secondes.
Informations affichées
- Résultats : magasins vendeurs (+ % du réseau), quantité réseau, quantité par
magasin, prix moyen réseau, CA par magasin, marge %, sparkline de tendance,
fournisseur, et présence ou non au catalogue Nancy.
- Fiche : carte « Performance réseau · 12 mois glissants » en tête (8 indicateurs
+ détail mensuel qté / magasins / qté par magasin / CA / prix moyen / marge %),
puis carte « Fournisseur » dédiée, puis identité, stock, nos ventes 12 mois.
- La fiche s'ouvre aussi par code centrale (?cc=) pour les produits que le réseau
travaille et que Nancy ne référence pas — le libellé et le fournisseur Qlik
sont persistés dans le cache réseau pour ce cas.
Tendance réseau
- computeNetworkTrend reconstruit sa fenêtre depuis la date du jour : 12 mois
complets, mois en cours EXCLU (il est partiel et écrasait la pente). Les mois
absents du cache valent 0 ; ceux antérieurs à la première extraction réelle
sont écartés au lieu d'être inventés à 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Nouvelle page /produits : recherche par libellé ou par code centrale, puis
fiche complète d'un produit — identité, prix, stock par magasin, ventes sur
12 mois glissants, et l'intégralité des métriques réseau Qlik.
Jusqu'ici les données produit n'étaient accessibles que par fournisseur
(la Grille) ou par classement (Hit Parade) : aucune route n'acceptait un
codein. Les données réseau Qlik n'étaient visibles que dans six colonnes
de la grille d'arbitrage.
Deux analyses ajoutées :
- comparatif local vs réseau, ramené au magasin moyen (indice de
performance sur les quantités, seule base commune fiable — le CA local
est TTC et la base du CA Qlik n'est pas documentée, donc indicatif) ;
- opportunités de la même famille : références que le réseau vend bien et
que nous vendons peu, écart calculé par magasin.
Détail mensuel réseau enrichi : le cube Qlik renvoyait déjà CA, nb de
magasins, CA/magasin et marge % par (article, mois) mais seule la quantité
était conservée. Ces mesures sont désormais stockées dans une nouvelle
colonne metrics_by_month, sans requête Qlik supplémentaire. qte_by_month
reste inchangée — la colonne « Tendance / Réseau » de la Grille en dépend.
L'extraction Qlik peut désormais cibler un seul code centrale
(/api/qlik/sync?codeCentrale=…), ouverte à tout utilisateur connecté et
plafonnée à 3 extractions simultanées ; le mode fournisseur reste
réservé aux admins.
Refactor sans changement de comportement : les helpers de mois, le calcul
de tendance et les graphes SVG quittent heatmap-grid.tsx pour des modules
partagés, et le polling des jobs Qlik devient le hook useQlikSyncJob,
réutilisé par la Grille et par la fiche.
Les calculs de ventes reprennent le SQL canonique de la Grille (genremvt=3
avec quantités niées pour déduire les retours, stock de fin de mois pris
sur le dernier mouvement, report sur les mois sans mouvement), afin que
les chiffres des deux pages soient identiques par construction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8cMbsdjXWChX8seL9NCP