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
Grille (navigateur)
- « Rafraîchir » ne passe plus par l'URL : le paramètre _refresh restait
mémorisé et chaque retour par le menu forçait un recalcul serveur complet
(migration du store pour nettoyer les valeurs déjà enregistrées).
- Lignes reçues regroupées toutes les 300 ms au lieu de reconstruire tout le
tableau à chaque paquet de 150 (chargement quadratique).
- Sélection indexée par code article (getRowId) : après un filtre, l'action
groupée visait d'autres produits. Seules les lignes affichées comptent.
- Recherche : un passage par ligne au lieu d'un par colonne ; formateurs de
nombres partagés (lib/format.ts) ; colonnes mensuelles mémorisées ;
plus de transition-all sur les lignes positionnées par transform.
- exceljs, jspdf et xlsx chargés au clic seulement.
Grille (serveur)
- Requêtes Qlik et snapshot lancées en parallèle de la phase SQL.
- Cache mémoire borné (LRU) ; calcul en cours réutilisé même en forcé ;
flux interrompu quand le client part.
- Après un enregistrement, les gammes sont reportées dans le cache et dans
grid_rows au lieu d'invalider (recalcul de ~40 s évité).
Données et API
- lib/ff-cache.ts : cache mémoire 30 min des lectures FF (stock, hit-parade,
CA mensuel, fournisseurs, dernière réception, publicités), vidé en fin de
synchro nocturne ; erreurs jamais gardées.
- Pool PostgreSQL unique (globalThis), réglage sans workers parallèles posé
une fois par connexion au lieu d'une transaction autour de chaque requête.
- Délai maximal sur tous les appels à l'API FF.
- Index session_snapshots ; commandes : plus de double rechargement ;
synchro : saisies regroupées ; paramètres : configuration lue une fois ;
journal de capture en ajout seul, rien de formaté sans capture ouverte.
Corrections
- Historique : l'échec de chargement s'affiche (au lieu de « Aucun snapshot »).
- Sessions et validations enregistrent le magasin affiché, plus « TOTAL ».
- Publicités : libellé du statut « passées ».
Plan complet : docs/plan-optimisation-webui.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
Le code fournisseur FF de la base SQL/API existe aussi dans Qlik : une
seule valeur y designe le meme perimetre que les dizaines de milliers de
codes articles. Comme SelectValues sur « Article Code » coute 8 a 100 s
par appel et doit etre refait a chaque passe annuelle, la bascule
supprime la partie la plus couteuse de la sync.
- ETAPE 0 : selectionnerParFournisseur() avant choisirPeriode().
- Essaie QLIK_FIELD_CODE_FOURNISSEUR puis code_fournisseur, Fournisseur,
fournisseur_code_centrale.
- Verifie la couverture reelle (Article Code visibles vs codes demandes)
et n'accepte la bascule qu'au-dela de QLIK_SUPPLIER_MIN_COVERAGE
(0.99 par defaut) ; sinon annule la selection et revient aux codes.
- Quand la bascule est retenue, plus aucun SelectValues sur Article Code
(passes annuelles et agregation par expressions).
- La sync fournisseur transmet job.fournisseur ; la sync produit (un seul
code centrale) reste inchangee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Deux remarques de l'utilisateur ont débloqué le sujet : « sur ce tableau j'ai
2024, 2025, 2026 et je sors mois par mois », et « tu mets 2026 et tu as la
comparaison N-1 ».
Type_Cal='N' ne désigne donc pas « l'année civile en cours » mais L'ANNÉE
SÉLECTIONNÉE. Il suffit d'enchaîner les années couvertes par la fenêtre (2025
puis 2026) pour reconstituer 12 mois glissants — chaque passe livrant les 5
mesures, et non la seule quantité comme « Quantité COMP ».
Ce mécanisme existait déjà (monthDimPath(yearN - 1)) mais restait sans effet :
[qlik-pw] (mois) passe N-1 terminée : 299905 → 299905 points mensuels
La sélection « Période », héritée de l'ouverture de l'app et jamais choisie par
nous (observé « Période:1/4 » dès le premier diagnostic de la session),
épinglait le contexte sur l'année en cours et annulait la sélection d'année.
- choisirPeriode() teste désormais EN PREMIER l'état sans aucune sélection de
Période — l'état dans lequel un utilisateur voit 2024/2025/2026 — et efface la
sélection dès que la couverture est incomplète.
- L'orchestration boucle sur les années de la fenêtre au lieu d'une passe
principale plus une passe N-1 dérivée de COMP.
- Chaque passe n'écrit que les mois DE LA FENÊTRE et les totaux s'additionnent
sur ces mois : deux années ne peuvent pas se doubler puisqu'elles ne partagent
aucun mois. « Quantité COMP » n'est plus utilisée.
- L'agrégation directe des faits reste en secours, et le garde-fou d'intégrité
reste actif : sans les 12 mois, le cache n'est pas écrit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Deux constats de la dernière sync.
1. AUCUNE valeur de « Période » ne porte 12 mois glissants — c'est mesuré :
« juin 2026 » 1/12, « Année à date » 6/12, « Mois à date » 0/12,
« Semaine 2026/30 » 0/12
Ce sont 4 contextes relatifs à aujourd'hui, pas des types de période. Les
master measures ne pourront donc jamais couvrir la fenêtre, et l'agrégation
directe des faits devient obligatoire.
D'où un bug du commit précédent : garder la meilleure Période (6/12) RESTREINT
les faits à 2026-01→06 et ampute l'agrégation directe, qui sait pourtant lire
tout l'historique. choisirPeriode() efface désormais la sélection quand la
couverture est incomplète, et ne la garde que si elle couvre les 12 mois.
2. Le cube à 5 mesures calculait 26 s puis rendait 0 ligne, alors que
Sum(quantite) seul donnait 35 424 324 : signature d'une mesure invalide qui
invalide tout l'hypercube, sans qu'aucun message ne dise laquelle.
validerExpression() teste maintenant chaque expression isolément sur un petit
cube [Mois] avant de construire le gros. Une expression invalide est remplacée
par la colonne neutre 0 — le cube reste exploitable et le log nomme la
coupable. Seule la quantité est bloquante.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Fusion de deux lignes de travail parallèles sur main, complémentaires et non
concurrentes :
cd2e77e selectionnerFenetreViaDimensionMois() — sélectionne les 12 mois par
qElemNumber sur la dimension maître Mois. C'est ce qui marche : les
champs Date exposés sont dissociés des faits (100 % du total avant
comme après), la dimension maître porte les vraies valeurs.
4ef0e9f garde-fou d'intégrité — refuse d'écrire le cache si la fenêtre n'est
pas entièrement couverte. C'est pourquoi le cache est resté inchangé
au lieu d'être corrompu.
726fb84 choisirPeriode() — le maillon manquant.
Le dump des expressions donne la cause de fond :
« Quantité N » = Sum({<Type_Cal={'N'}>} quantite)
« Quantité COMP » = Sum({<Type_Cal={'$(vPeriod_comp)'}…>} quantite)
Un pont de périodes duplique les lignes de faits ; Type_Cal marque la période
analysée (N) ou de comparaison (COMP), et le champ Période (4 valeurs) décide
de quelle période il s'agit. Sélectionner correctement les mois ne suffit donc
pas : sous la période par défaut, « Quantité N » rend 0 sur tout mois hors
période courante, même parfaitement sélectionné.
Orchestration fusionnée :
1. choisirPeriode() essaie chaque valeur de Période et MESURE, sur un cube
[Mois] × Quantité N, combien de mois de la fenêtre elle rend disponibles.
Aucun libellé n'est deviné.
2. Si une période couvre les 12 mois → master measures (exactes sur toute la
fenêtre). Sinon → agrégation directe des faits, en secours.
3. Dans les deux cas, la couverture des 12 mois est revérifiée avant
persistance ; sinon l'extraction est refusée et le cache laissé intact.
La tendance conserve la règle stricte de 4ef0e9f (les 12 clés explicitement
présentes, ou pas de tendance), qui écarte aussi le « +4100 % » calculé sur deux
points pour un article vendu depuis mai.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Le dump des expressions donne enfin la cause de fond :
« Quantité N » = Sum({<Type_Cal={'N'}>} quantite)
« Quantité COMP » = Sum({<Type_Cal={'$(vPeriod_comp)'}…>} quantite)
« Magasin Ventes Nb N » = Count({<Type_Cal={'N'}>} Distinct ventes_code_site)
L'app n'est pas un modèle « faits + calendrier ». Un pont de périodes duplique
les lignes de faits : Type_Cal='N' marque celles de la période analysée, 'COMP'
celles de la période de comparaison, et le champ Période (4 valeurs, une
sélectionnée) décide de quelle période il s'agit. Tout en découle :
- sélectionner Date ne change rien (mesuré : 100 % du total avant comme après),
le périmètre venant du pont de périodes et non du champ date ;
- « Quantité N » ne couvre que la période courante, d'où août→décembre vides ;
- « Quantité COMP » ne couvre que les mois ayant un comparable, d'où
janvier→juillet de l'année précédente ;
- Sum(quantite) brut additionne les lignes N ET COMP : il double compte, il ne
pouvait pas servir de substitut.
choisirPeriode() essaie donc chaque valeur de Période, mesure sur un cube
[Mois] × Quantité N combien de mois de la fenêtre elle rend réellement
disponibles, et garde la meilleure. Aucun libellé n'est deviné : c'est la
couverture mesurée qui décide, et elle est tracée. Si aucune valeur ne couvre
les 12 mois, les mois manquants restent absents du cache.
Aussi :
- Les mois réellement couverts reçoivent un zéro EXPLICITE quand l'article n'a
pas vendu ; la tendance ne retient que les mois présents comme clés. Un article
vendu depuis mai voyait sa tendance calculée sur deux points (« +4100 % »).
- Les diagnostics du chemin d'extraction sont répétés en fin de sync
([qlik-pw][diag]) : ils étaient au début d'un log de plusieurs milliers de
lignes.
- QLIK_USE_EXPR passe à désactivé par défaut.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Le log de la dernière sync montre batches=3 et des lignes « (rattrapage) » : le
chemin par expressions a bien tourné mais a été rejeté (quantité totale nulle),
et c'est le rattrapage qui a de nouveau recopié le total de période dans les mois
manquants — d'où les plateaux à 87 648 d'août à décembre et les « -162 % ».
- Passe de rattrapage SUPPRIMÉE. Elle ré-extrayait un mois vide en ne
sélectionnant que ses dates ; les master measures ignorant la sélection Date,
le cube lui renvoyait le total de période. Un mois qu'on ne sait pas extraire
doit rester absent, jamais rempli d'une valeur plausible.
- Le chemin par expressions vérifie maintenant qu'un champ date filtre RÉELLEMENT
les faits, ce que personne n'avait pu contrôler tant que seules des mesures
insensibles à la sélection étaient utilisées. Un total de contrôle
(Sum(quantite) sans dimension) est comparé avec et sans filtre ; un champ n'est
retenu que si le total est non nul ET strictement inférieur au total global.
Candidats : QLIK_DATE_FIELD, Date, Date calendrier, Date_Key (ce dernier
sélectionné au format AAAAMMJJ). Si aucun ne filtre, l'extraction le dit
clairement au lieu de produire une fenêtre fausse.
- Diagnostic : échantillon BRUT du cube loggué avant tout filtrage (mois brut,
mois normalisé, appartenance à la fenêtre, valeurs des cinq mesures), et
compteur de lignes écartées comme hors fenêtre. Ces deux traces distinguent une
expression invalide d'un format de dimension Mois inattendu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Le serveur Qlik est filtré par IP : injoignable depuis l'extérieur du réseau FF
(la passerelle d'egress tombe en connection timeout sur 443 comme sur 80, alors
qu'elle atteint le reste d'Internet). Impossible donc de valider à distance les
expressions par défaut ni la clé de jointure.
Ce script répond aux deux questions en une exécution, à lancer depuis le
conteneur de l'app :
- expressions réelles des master measures (CA N, Quantité N, COMP…) ;
- tableau mois par mois comparant Sum(quantite) / Sum(ca_ht) / Sum(ca_ttc) /
Count(DISTINCT [Magasin Code]) aux mesures « N », sur l'année en cours (où
elles doivent coïncider) et sur l'année précédente (où les « N » sont
censées être à 0) ;
- Article Code, article_no_centrale et article_codein côte à côte, pour
trancher la jointure avec articles.artcentrale.
Lecture seule : sélections en soft lock, objets de session détruits. Le mot de
passe vient de l'environnement, rien n'est écrit ni affiché.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Preuve arithmétique dans les logs : pour l'article 10000397784, qteReseau=164 et
caReseau=2220,75 € sont EXACTEMENT la somme de janvier à juillet 2026
(12+19+24+27+22+31+29 = 164). Et la passe de rattrapage, avec août 2025 seul
sélectionné, a renvoyé ces mêmes 164 / 2220,75 — puis les a recopiés dans les
cinq mois manquants.
« CA N » et « Quantité N » sont donc des mesures « cumul année en cours »,
insensibles au champ Date. Aucune sélection ne peut leur faire produire une
fenêtre 12 mois glissants. D'où, simultanément :
- des totaux réseau qui étaient un cumul année en cours, mois courant partiel
inclus, et non 12 mois glissants ;
- les mois de l'année précédente non couverts par « Quantité COMP » vides sur
tous les articles ;
- un rattrapage qui y recopiait le total de période.
Le chemin nominal agrège désormais directement les champs de faits, qui eux
respectent les sélections : Sum(quantite), Sum(ca_ht),
Count(DISTINCT [Magasin Code]), Sum(marge) — tous surchargeables par
QLIK_EXPR_QTE / _CA / _NBMAG / _MARGE. Un seul cube [Article Code, Mois] couvre
les 12 mois : ni passe N-1 ni rattrapage, c'est ce qu'ils compensaient. Les
totaux deviennent la somme des 12 mois de la fenêtre.
Garde-fous : « Quantité N » reste dans le cube pour calibrage et le log compare
les deux sur les mois de l'année en cours (seul périmètre où elle est juste) ;
quantité totale nulle ou erreur Engine → repli automatique sur l'ancien chemin ;
QLIK_USE_EXPR=0 le force.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Les timings de production sont sans appel : SelectValues sur « Article Code »
coûte 8 à 100 s PAR APPEL, quasi indépendamment du nombre de valeurs (le moteur
balaie un symbole de plus d'un million d'entrées). Avec 7 200 codes en lots de
300, cela faisait 24 appels, ~15 min de sync, et la session Qlik mourait avant
la fin — « Socket closed » puis « Execution context was destroyed » — en perdant
la totalité de l'extraction.
- Le chemin mensuel fait désormais UNE seule sélection pour tous les codes et un
seul cube paginé : la dimension Mois livre déjà tous les mois d'un coup, rien
n'obligeait à découper. La lecture des pages coûte ~50 ms. Repli automatique
sur les lots si l'Engine refuse (les lignes déjà lues sont retirées pour ne
pas doubler les totaux).
- rattraperMoisVides() adopte le même principe : une sélection de codes, puis
seule la fenêtre de dates change d'un mois à l'autre.
- Sonde de diagnostic de fin de sync retirée : elle refaisait une sélection
complète des codes et des 365 jours pour rien.
- Point de contrôle (cfCheckpoint) poussé après chaque passe. Si la session
meurt, l'extraction repart de là : les codes déjà traités ont des données
complètes, seul le reliquat manque. Le log signale « résultat PARTIEL ».
- Le script in-page remonte aussi ses lignes en cas d'erreur interne, et
« rows=[object Object] » dans les logs est corrigé.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Cinq mois consécutifs à 0 sur TOUS les articles n'est pas un résultat métier.
Les mesures « N » de l'app Qlik sont bornées à une année civile ; le chemin
« dimension Mois » sélectionne les 365 jours d'un coup, donc la mesure ne se
résout que sur une année et les mois de l'année précédente ressortent vides.
« Quantité COMP » ne rattrape que les mois ayant un comparable dans l'année en
cours — d'où le trou d'août à décembre 2025 observé en juillet 2026.
- La passe principale n'écrit plus de 0 pour un mois hors année N, et COMP
n'écrit rien quand elle est vide : un mois absent peut être rattrapé, un mois
à 0 se lit comme « pas de vente » et masque le trou définitivement.
- Nouvelle passe rattraperMoisVides() : tout mois resté vide pour tous les
articles est ré-extrait en ne sélectionnant QUE ses dates. La mesure se résout
alors sur la bonne année quelle que soit l'écriture de son set analysis. Elle
ne touche que le mensuel, jamais les totaux de période.
- Les expressions des master measures sont dumpées au log : sans elles,
impossible de savoir comment la mesure est bornée dans le temps.
Contournement sans redéploiement : QLIK_MONTH_DIM=0 (itération mois par mois,
plus lente mais sans trou). Les données déjà en cache gardent leurs zéros — il
faut relancer la sync pour les corriger.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
La sync de la Grille marche parce qu'elle est SEULE sur le serveur Qlik. La
recherche, elle, ouvrait une seconde session concurrente pour les mesures
pendant que la première tenait une énorme sélection : l'Engine coupait les
requêtes (code 15 « Request aborted »).
- Les mesures réseau (CA, quantité, magasins, CA/magasin, marge %) sortent
désormais du MÊME cube que l'identité, dans la même session : la fenêtre 12
mois glissants est sélectionnée sur le champ Date, les master measures sont
résolues par titre comme dans l'extraction de la Grille, et le cube est trié
par quantité réseau décroissante. Plus d'appel à
fetchNetworkMetricsPlaywright pendant une recherche.
- Sélection plafonnée à 300 libellés au lieu de 1 000 : sélectionner les 13 446
libellés contenant « tapis » prenait 81 s puis 486 s. Le cube étant trié, 300
suffisent pour remonter les meilleures ventes.
- Une seule recherche simultanée (deux jobs en parallèle aggravaient la
saturation).
- Le détail mensuel vient du cache : le cube de recherche est agrégé. L'upsert
passe les colonnes mensuelles en COALESCE pour qu'une recherche n'efface pas
un mensuel déjà extrait par une sync.
Jointure catalogue : les 40 codes ne trouvaient aucune correspondance dans
articles.artcentrale (formats différents — « Article Code » renvoie 8 chiffres,
artcentrale en a 11). Le champ article_no_centrale est maintenant remonté comme
seconde clé candidate, la jointure essaie les deux, et un échantillon des deux
clés est loggué quand aucune ne joint.
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
Les logs de production ont tranché les trois causes.
1. La recherche de list object Qlik applique un OU entre les mots : « tapis
anti » ramenait 17 696 libellés sur 1 070 645 — presque tous ne contenant
que « anti ». On cherche désormais mot par mot pour retenir le plus sélectif,
on filtre en ET côté client (tous les mots présents, insensible
casse/accents), et on ne sélectionne que les valeurs retenues.
2. Sélectionner ces 17 694 valeurs faisait abandonner le cube suivant
(code 15 « Request aborted »), d'où le repli systématique sur le catalogue
local. La sélection est maintenant plafonnée à 1 000 valeurs, faite par
numéro d'élément (SelectListObjectValues) plutôt que par texte, et les
appels Engine retentent trois fois sur code 15.
3. Les champs détectés étaient les mauvais : article_libelle_ticket (libellé
ticket tronqué) au lieu d'Article, et code_fournisseur (le code, d'où les
« F005 » affichés) au lieu de Fournisseur. Une liste de préférences précède
désormais l'heuristique, avec repli automatique sur le candidat suivant si
un champ ne ramène rien.
Aussi :
- Un repli catalogue n'est plus mis en cache 10 min : une panne Qlik passagère
se corrigeait uniquement avec « Relancer ».
- Le code 15 est traduit en message actionnable, et un terme trop large pour le
moteur est signalé au lieu d'un « aucun résultat » trompeur.
- Les logs de la page ne relaient plus le bruit du client Qlik (thèmes,
extensions).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
Les intitulés des champs varient d'une app Qlik à l'autre. La détection reste
heuristique, mais l'inventaire complet des champs est désormais loggué et deux
variables d'environnement permettent de corriger le tir sans redéploiement de
code : QLIK_FIELD_ARTICLE_LIBELLE et QLIK_FIELD_FOURNISSEUR.
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