mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
7da4c6d5fd4061e662828b15391ad5d10d4b5f7d
532
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7da4c6d5fd |
perf: lot 1 — chargements plus rapides et corrections, sans changement visuel
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 |
||
|
|
2c84cfda53 |
perf(stock): onglets instantanés, pagination et fournisseur de dernière entrée
Le changement d'onglet déclenchait un router.push : la page serveur était rejouée et les 3 requêtes SQL relancées à chaque clic, alors que les données étaient déjà côté client. Sans aucun retour visuel, l'onglet semblait bloqué. Interface (stock-negatif/client.tsx) - Changement d'onglet purement client (useTransition + history.replaceState) : plus aucune requête SQL rejouée, l'URL reste partageable. - Modal de chargement pendant le changement de magasin (seule action qui interroge vraiment le serveur), pendant un changement d'onglet lent et pendant la génération de l'export Excel. Affichage différé de 120 ms pour éviter tout clignotement. - Pagination 50 lignes : l'onglet « Sans vente 6 mois » rendait jusqu'à 17 000 lignes d'un coup, ce qui figeait le navigateur. L'export Excel continue de reprendre l'intégralité des lignes filtrées. - Tri : ordre de la base conservé tant qu'aucune colonne n'est cliquée (le tri alphabétique sur codein écrasait le classement métier), collateur Intl réutilisé au lieu d'un localeCompare par comparaison, tri numérique robuste aux valeurs nulles. - Les 3 onglets partagent désormais un composant de tableau unique piloté par une configuration de colonnes, au lieu de 3 copies quasi identiques. - Select magasin et onglets désactivés pendant le chargement, aria-busy posé. Données (pg-ff-client.ts) - Un article n'est plus rattaché à son fournisseur principal mais au fournisseur de sa dernière entrée en stock : s'il est rentré chez un autre fournisseur, il n'apparaît plus sous le précédent. La colonne de mvtart qui porte ce lien est détectée une fois par process parmi une liste blanche (même principe que getNomenclatureParentCol), avec repli documenté sur artfou1.preference = 1 si la base ne porte pas l'information. - La jointure latérale sur la dernière entrée remplace la sous-requête corrélée MAX(datmvt) : même coût qu'avant pour une information de plus. - pgGetStockSansVente dédoublonne artfou1 via DISTINCT ON comme ses deux requêtes sœurs, et exclut les stocks nuls en SQL (HAVING) au lieu de les filtrer côté client : le compteur d'onglet correspond enfin aux lignes réellement transmises. Diagnostic - GET /api/diag/stock-fournisseur : colonne détectée, colonnes réelles de mvtart et échantillon comparant fournisseur principal et fournisseur de dernière entrée. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174nm1nWPaypWBNHioSTJ8X |
||
|
|
585e171708 |
fix(grid): aligner la fenêtre 12 mois sur les données et tabuler les ventes du modal
Le total « Tot. 12m » est figé côté serveur sur SA fenêtre de 12 mois, tandis que la Grille et le modal recalculaient les 12 mois depuis l'horloge du navigateur. Dès que les deux divergeaient (onglet ouvert au changement de mois, lignes servies depuis le cache), un mois de ventes disparaissait des colonnes tout en restant compté dans le total : 28 sur la ligne, 20 dans le modal. - months.ts : `getMonthsFromRows()` lit la fenêtre dans les clés des séries reçues ; `getLast12Months()` ne sert plus que de repli avant chargement. - heatmap-grid : MONTHS_12 dérivé des lignes chargées. - product-monthly-modal : le tableau par magasin devient générique et sert aussi aux ventes, sous le graphique, avec une colonne de cumul « 12 m » qui retombe sur le total de la Grille. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177HLQz6w2hgoXpx6t1Rkn9 |
||
|
|
2404b22628 |
feat(grille): fige les colonnes d'identité à gauche
Sur 2600 px de tableau pour ~1150 px visibles, on voit 8 colonnes sur 18. Dès qu'on défilait vers les totaux ou les mois, la Désignation sortait de l'écran et plus rien ne disait quelle ligne on lisait. Case à cocher, Code interne et Désignation restent désormais collés au bord gauche pendant le défilement horizontal, en-tête compris. Deux conséquences de conception : - La Désignation a été rapprochée du Code interne dans l'ordre des colonnes. Le figeage exige des colonnes contiguës depuis la gauche ; en la laissant après Référence et GTIN, il aurait fallu geler ces deux-là aussi, soit 636 px immobilisés au lieu de 406. L'ordre « code, désignation, puis codes secondaires » est de toute façon celui qu'on lit. - Les cellules figées doivent être OPAQUES, le reste de la ligne glissant dessous. Le survol passe donc par une teinte pleine et non par le voile translucide des cellules ordinaires, et l'en-tête figé reprend le dégradé du thead pour que la bande reste d'un seul tenant. Un filet marque la limite de la zone figée. Les décalages sont calculés par cumul des largeurs réelles et s'arrêtent à la première colonne non figée : masquer le Code interne réajuste tout seul, et une colonne figée ne peut jamais se retrouver à flotter au milieu du tableau. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
7f5ef93469 |
fix(grille): défilement horizontal visible et barre du bas dans le flux
Les deux symptômes n'en faisaient qu'un. L'ascenseur horizontal EXISTAIT déjà (conteneur en overflow-auto, table en min-width ~2600 px), mais il était caché sous la carte de synthèse. Celle-ci était en `position: fixed` et ne réservait donc aucune hauteur : seul un `pb-12` posé sur la page compensait, trop court d'une quarantaine de pixels — exactement la bande où se trouvent la barre de défilement et la dernière ligne. Son `left-[264px]` était en prime calé sur une barre latérale dépliée, alors qu'elle est repliée par défaut : 200 px de vide à gauche, qui écrasaient ses statistiques en hauteur. Elle est remise dans le flux : la grille lui cède exactement la place nécessaire, et il n'y a plus de valeur magique à tenir en phase avec la sidebar. L'ascenseur devient visible : la règle générale (8 px, rail transparent, pouce à 12 % d'opacité) convient aux panneaux où le défilement est accessoire, pas à la Grille où il est l'outil de navigation principal. Classe `.grid-scroll` dédiée, avec rail dessiné, pouce saisissable, et prise en charge de Firefox que les sélecteurs -webkit- ignorent. Corrigé aussi : - totalWidth lisait `columnDef.size` au lieu de `getSize()`. Le redimensionnement de colonnes étant actif et persisté, la largeur plancher restait figée sur l'ancienne somme : dernières colonnes rognées, course de défilement trop courte. - Conteneur non focusable : flèches, Origine et Fin ne défilaient pas la grille. - Barre de filtres qui passait sur deux lignes sous ~1200 px, volant 48 px au tableau : largeurs réduites en dessous de xl. - Barre d'actions groupées sans flex-wrap, qui s'écrasait sur petit écran. - Double croix dans la recherche : `type="search"` ajoutait la croix native du navigateur par-dessus le bouton maison. Masquage local — la recherche de l'en-tête n'a pas de bouton propre et garde donc le sien. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
828a50bfc0 |
Trous d'assortiment : la référence à côté du code interne
Le code interne identifie l'article chez nous, la référence l'identifie chez le fournisseur : c'est elle qu'on recopie sur une commande. La fenêtre ne donnait que le premier, obligeant à revenir à la Grille pour retrouver le second — l'export, lui, portait déjà les deux. Référence absente affichée « — » plutôt que laissée vide : une cellule vide se lit comme une colonne qui n'a pas fini de charger. Vérifié sur build de production, avec des références longues et absentes : colonne lisible, aucun débordement horizontal de la fenêtre. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR |
||
|
|
f75d51f6f4 |
Trous d'assortiment : extraction Excel de la liste affichée
Un bouton « Export Excel » dans la fenêtre des produits non travaillés. Il reprend EXACTEMENT ce qui est à l'écran — même magasin, même profondeur, même classement — plutôt que de recalculer une liste : un fichier qui ne correspond pas à la fenêtre d'où il sort ne se vérifie plus. Deux feuilles. Les données seules d'abord : en-têtes en ligne 1, filtre automatique, volet figé, et surtout des valeurs NUMÉRIQUES — le tableur doit pouvoir trier et sommer, ce qu'une chaîne « 89 863 € » lui interdit. La mise en forme des nombres suit le critère de tri (euros, unités, pourcentage). Le contexte ensuite, sur sa propre feuille : fournisseur, magasin examiné, classement retenu, définition du « non travaillé », date. Mêler les deux dans une feuille unique aurait condamné filtre et tri. `exceljs` est chargé à la demande plutôt qu'importé en tête : il pèse lourd, et la Grille n'a pas à le transporter pour tous ceux qui n'exportent jamais. Un échec de génération s'affiche dans la fenêtre — un export qui échoue en silence laisse croire au téléchargement. Le nom du fournisseur descend jusqu'à la fenêtre pour nommer et documenter le fichier. Vérifié sur build de production, en téléchargeant le fichier puis en le relisant : « Non_travailles_Fournisseur_Test_Houdemont_Top200_2026-08-14 », deux feuilles, 15 lignes conformes au décompte affiché, filtre automatique sur A1:J1, valeurs lues comme des nombres et format « #,##0 "€" » sur la colonne du critère. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR |
||
|
|
beefb07ba0 |
Grille : les produits qu'un magasin ne travaille pas
Un bouton « Non travaillés » ouvre, sur le haut du classement AFFICHÉ, la liste des produits qu'un magasin ne travaille pas. Le classement de référence est celui du tableau à l'instant où l'on ouvre : le tri en cours — CA réseau, quantité, marge, peu importe — fixe l'ordre, et la valeur qui a servi à classer est relue sur la colonne triée pour être affichée en regard. Recalculer un ordre maison aurait répondu à une autre question que celle qu'on vient de poser à l'écran. « Non travaillé » = aucune vente sur 12 mois glissants ET aucun stock au dernier mois connu. Les deux conditions comptent : un produit sans vente mais en stock est détenu par le magasin — c'est un invendu, pas un trou d'assortiment, et le mélanger aux vrais trous ferait commander ce qui dort déjà en rayon. Chaque ligne porte son rang, la valeur du critère de tri, et ce que les AUTRES magasins en font : sans les ventes d'à côté, un trou ne se distingue pas d'un produit que personne ne vend. Une dernière colonne sépare « jamais détenu » de « déjà détenu, sans vente » — le premier est une piste, le second une tentative déjà faite. Profondeur réglable (100 / 200 / 500), magasin au choix parmi ceux réellement présents dans les données. Le classement n'est extrait que fenêtre ouverte, et tronqué à la profondeur maximale : sans quoi on copierait 130 000 lignes pour en regarder 500. Vérifié sur build de production, quatre profils de produits : à Houdemont seuls remontent les jamais détenus, à Frouard seuls les déjà détenus sans vente, et le profil « stock sans vente » n'apparaît dans aucun des deux. Les rangs affichés correspondent aux positions dans le classement trié par CA réseau. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR |
||
|
|
06f0bd0513 |
Prix de vente : tenir compte d'un prix non ventilé par magasin
`cube_pv` a une clé `(artnoid, site)`, mais la base n'en porte qu'une ligne par article — 428 530 lignes pour 427 857 articles, là où deux magasins tarifés en donneraient le double. Le prix n'est donc pas ventilé : la colonne serait restée vide dès qu'on consulte le magasin qui n'a pas la ligne, alors même que l'article a un prix. La cellule couvre maintenant les quatre cas : le prix du magasin consulté quand il existe ; le prix unique quand il n'y a qu'un tarif, avec la mention « prix unique, non ventilé par magasin » ; rien quand plusieurs prix coexistent sans concerner ce magasin — celui du voisin n'est pas le sien ; et, en « tous magasins », le prix commun ou le plus élevé assorti du « ≠ ». Vérifié sur build de production, quatre articles couvrant les quatre cas, dans les trois modes de magasin. Le cas décisif : prix porté par le seul site 292, consulté depuis Houdemont — 12,90 € et la mention, au lieu du « - » d'avant. La route de diagnostic est retirée : `cube_pv` est confirmé alimenté et synchronisé, elle a fait son office. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR |
||
|
|
8158eda11b |
Prix de vente : élaguer le code magasin, qui sert de clé
Le code magasin de `cube_pv` sert de clé de recherche côté Grille (`prixVenteByStore["292"]`). Or la synchronisation Apiflow l'écrit BRUT (`site: r.Site`), là où elle fait passer celui de `cube_stock` par `safeStr` — et aucune des deux ne l'élague. Une colonne MSSQL de largeur fixe remonte complétée d'espaces : la recherche échouerait alors sans rien signaler, et de la pire façon qui soit, le prix restant visible en « tous magasins » et vide magasin par magasin. `TRIM` des deux côtés, plus l'élagage du repli lu depuis le cube de stock. L'opération ne coûte rien quand il n'y a pas de remplissage, et le doute n'avait pas de raison de subsister. La route de diagnostic affiche désormais les valeurs de `site` telles qu'elles sont stockées, avec leur longueur : de quoi vérifier d'un coup d'œil que les clés sont bien « 292 » et « 579 », sur trois caractères. Commentaires SQL déplacés hors du littéral gabarit, comme partout ailleurs dans le fichier : un accent grave dans un commentaire refermait la chaîne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR |
||
|
|
c9e1934aae |
Grille : le prix de vente vient de cube_pv, pas d'article_infosup
La colonne PV s'affichait vide sur toutes les lignes. Le schéma du SaaS Postgres (dépôt Apiflow, postgres/init.sql) explique pourquoi : la seule source lue jusqu'ici, `article_infosup.prix_vente_mini`, est une borne de PARAMÉTRAGE — un prix plancher, à côté de `prix_vente_maxi` et `pv_conseille` — et non le prix pratiqué. Le prix de vente vit dans `cube_pv (artnoid, site, pv)`, pendant exact du `cube_pa` déjà utilisé pour l'achat, rafraîchi chaque nuit depuis le cube MSSQL `Cube_PV`. `cube_stock` porte le même PV et sert de filet : il ne couvre que les articles ayant une ligne de stock, mais il est déjà interrogé par la Grille, une colonne de plus dans le SELECT suffit. Le prix est propre à chaque MAGASIN. La colonne suit donc le magasin consulté ; en « tous magasins » elle montre le prix commun, et s'ils divergent le plus élevé assorti d'un « ≠ » et de l'infobulle qui donne les deux. Une moyenne afficherait un prix qu'aucune caisse ne pratique, et retenir silencieusement l'un des deux ferait passer le prix d'un magasin pour celui des deux. L'en-tête devient « PV / Magasin » : « PV central » décrivait la fiche article, ce n'est plus la source. Vérifié sur build de production, prix identiques, divergents et absents : en « tous magasins » 14,90 € ≠ avec l'infobulle « Frouard (Nancy) : 13,90 € · Houdemont : 14,90 € » ; sur Frouard 13,90 €, sur Houdemont 14,90 €, sans marqueur ; « - » quand aucune source n'a de prix. La route de diagnostic compte désormais les trois sources côte à côte, de quoi confirmer sur la base réelle laquelle est renseignée. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR |
||
|
|
9729b00dc7 |
Diagnostic : d'où vient (ou ne vient pas) le prix de vente
La colonne « PV central » s'affiche vide sur toutes les lignes. Le chemin du code est pourtant intact : `getProductRows` renseigne `prixVente` depuis `article_infosup.prix_vente_mini` (phase 3), aucune phase suivante ne l'efface, et la route de streaming diffuse la ligne entière sans filtrer ses champs. La valeur est donc absente à la source. Deux causes possibles, que seule la base peut départager : la colonne existe mais n'est pas renseignée, ou le prix de vente vit ailleurs sous un autre nom. `article_infosup.prix_vente_mini` est la seule source de prix de vente câblée dans l'application — la Grille et la fiche produit la partagent, si bien qu'une fiche produit affichant « — » sur PV central confirmerait la première hypothèse. Cette route de diagnostic répond aux deux questions : remplissage réel de la colonne actuelle (absente / à zéro / renseignée), globalement puis pour un fournisseur donné, et énumération des colonnes candidates du schéma AVEC leur remplissage — une colonne bien nommée mais vide ne servirait à rien. À supprimer une fois la source établie. Elle est derrière l'authentification et ne renvoie que des comptages et quelques exemples. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR |
||
|
|
52b663579f |
Grille : colonnes de prix, et menu « Vues » pour les blocs de colonnes
Deux colonnes s'ajoutent à la suite de la tendance réseau :
· PV moyen (Réseau) — calculé par l'application, CA réseau ÷ quantité
vendue, exactement la définition déjà retenue par la fiche produit.
Sans quantité sur la période, la colonne affiche « - » : le rapport
n'existe pas, et un prix à 0 € se lirait comme un article donné.
· PV central (Article) — le prix de vente de la fiche article, lu en base
(article_infosup.prix_vente_mini), sans retraitement.
Les deux restent deux colonnes. En dériver un écart chiffré supposerait une
assiette commune — HT/TTC, remises — que les deux sources ne garantissent
pas ; le rapprochement se fait à l'œil, en connaissance de cause.
Le bouton de repli du bloc mensuel devient un menu « Vues » à deux entrées,
ventes mensuelles et prix, chacune repliable. Il agit par BLOCS, là où le
menu « Colonnes » voisin agit colonne par colonne : douze cellules
mensuelles ou deux prix ne se masquent pas une par une. Les deux états sont
persistés.
Au passage, le tri des colonnes réseau était faux. La sentinelle ±Infinity
choisie d'après le sens de tri courant ne pouvait pas fonctionner : TanStack
mémorise le résultat de l'accesseur dans `row._valuesCache` et ne le
réévalue jamais, si bien que la valeur calculée au premier tri restait figée
et remontait les lignes sans donnée en tête dès qu'on inversait le sens.
`sortUndefined: "last"`, traité avant l'inversion, les garde en bas dans les
deux sens. Les cinq colonnes réseau existantes en bénéficient, et `sorting`
quitte les dépendances des colonnes : plus de reconstruction complète à
chaque clic d'en-tête.
Vérifié sur build de production : les deux colonnes se placent bien après la
tendance, « - » sur quantité réseau nulle comme sur PV central absent, et le
tri place les absents en bas au premier clic (décroissant) comme au deuxième
(croissant). Menu « Vues » : 31 colonnes avec les deux blocs, 29 sans les
prix, 17 sans rien, 19 avec les prix seuls — chaque fois sur toutes les
lignes montées, sans rechargement, et l'état survit au rechargement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR
|
||
|
|
0a2bc8318b |
Grille : remonter le corps du tableau au changement de colonnes
Le repli du bloc mensuel ne repeignait toujours pas les lignes déjà montées : l'en-tête perdait ses douze colonnes, le corps les gardait, et il fallait recharger la page pour retrouver une grille cohérente. Le signal passé aux lignes (`columnsKey`) reposait sur la comparaison de props de `React.memo`. La clé du `<tbody>` porte désormais cette même signature : un changement de colonnes remonte le corps, sans plus dépendre d'une comparaison qu'une prop oubliée suffit à mettre en défaut. Le mécanisme existait déjà pour la densité d'affichage. Le coût est celui des seules lignes virtualisées (une trentaine), et le redimensionnement d'une colonne ne déclenche rien : les identifiants de colonnes, eux, ne changent pas. Vérifié cette fois sur un BUILD DE PRODUCTION, 2 000 lignes : au repli, 17 en-têtes et 17 cellules pour chaque ligne montée ; toujours 17 après défilement vers des lignes virtualisées ensuite ; 29 partout au retour, et 17 au repli suivant — sans rechargement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR |
||
|
|
c532386385 |
Grille : repeindre les lignes montées quand le jeu de colonnes change
Replier le bloc mensuel ne vidait que l'en-tête. Les lignes déjà montées gardaient leurs douze cellules, seules celles virtualisées ensuite adoptaient le nouveau jeu — une grille à deux vitesses, en-tête d'un côté, corps de l'autre. TanStack met ses objets `Row` en cache sur la seule identité des DONNÉES : changer les colonnes ne leur donne aucune référence neuve, et `React.memo` concluait qu'il n'y avait rien à repeindre. Le contournement existait déjà pour `columnVisibility` et `columnSizing`, passés en props sans jamais être lus ; le drapeau du bloc mensuel, lui, n'y figurait pas. `columnVisibility` cède la place à `columnsKey`, la signature des colonnes réellement visibles : elle change pour TOUT changement de colonnes, case décochée dans le menu comme bloc replié, là où l'ancien objet ne voyait que le menu. Calculée une fois par rendu de grille, pas une fois par ligne. Vérifié en rendant la Grille sur données de démonstration : 29 cellules par ligne et 29 en-têtes, 17 et 17 une fois le bloc replié, 29 et 29 au retour. Avec le signal figé, l'en-tête tombe à 17 quand les lignes restent à 29 — le défaut signalé. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR |
||
|
|
2ae523a804 |
Grille : détail mensuel au clic sur le total, bloc mensuel repliable
La colonne « Tot. 12m » dit combien, jamais comment. Un clic sur la case ouvre désormais le détail des 12 mois glissants, empilé dans l'ordre où on le lit : les ventes mensuelles, puis les entrées en stock, puis le stock de fin de mois magasin par magasin — un stock de 40 réparti sur deux sites et un stock de 40 bloqué sur un seul ne se pilotent pas pareil. Les deux premières séries suivent le magasin actif de la Grille, par cohérence avec la case cliquée ; la ventilation par magasin reste entière, c'est son objet. Les barres se dessinent de part et d'autre d'un zéro explicite : une quantité mensuelle peut être négative (retours supérieurs aux ventes) et sortirait sinon du cadre par le bas. Le bloc des douze colonnes mensuelles se replie d'un bouton. L'état est un drapeau à part, et non douze entrées de `columnVisibility` : masquer douze colonnes une par une n'est pas une manipulation, et les identifiants `month_YYYYMM` changent à chaque nouveau mois alors que le drapeau, lui, survit à la fenêtre glissante. Les colonnes sont retirées du modèle plutôt que cachées — inutile de faire calculer douze cellules par ligne à TanStack pour ne rien peindre. `TuileStat` sort de heatmap-grid.tsx vers un module partagé, la modale de tendance réseau et la nouvelle modale s'en servant toutes deux. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR |
||
|
|
92769535dc |
style(grille): agrandit la carte de tendance, illisible en l'état
Les textes du graphique tournaient entre 8,5 et 10 px : titres de séries, « max N », valeurs annotées, mois de l'axe, infobulle, ainsi que les légendes des trois tuiles. Difficile à lire. Typographie relevée d'un cran partout : titres de séries 9,5 → 13, maximum 9 → 11,5, valeurs sur les points 9 → 12,5, mois 8,5 → 11, infobulle 9 → 11,5, tuiles 10 → 12 avec la valeur à 26 px, tableau 11 → 13. Le dessin suit la typographie plutôt que de la subir : bandeau de facette 15 → 21, axe 18 → 24, écart 12 → 14, infobulle élargie à 196 avec un interligne de 17 — sans quoi titres et courbes se seraient chevauchés. La carte passe à max-w-2xl pour absorber le tout sans comprimer le graphique. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
a4e3c53dcc |
feat(grille): détail par magasin dans la carte, en mode tous magasins
En « tous magasins », les trois tuiles de la carte sont des cumuls : savoir que le stock est de 40 ne dit pas s'il est réparti entre Frouard et Houdemont ou entièrement sur un seul site — or c'est précisément ce qu'on vient vérifier. La carte affiche désormais, sous les cumuls, une ligne par magasin avec ses ventes, ses entrées et son stock de fin de mois. Uniquement en mode tous magasins : sur un magasin unique, les tuiles sont déjà ce détail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
6e59458403 |
fix(grille): grille vide au changement de magasin, et stock TOTAL incomplet
Trois défauts distincts, dont un que je venais d'introduire.
1. RÉGRESSION — grille vide. Le store zustand est persisté dans le navigateur
sans version ni migration. Le passage de filters.code3 à `string[] | null`
laissait donc une CHAÎNE dans le localStorage des utilisateurs :
`new Set("320211")` produit un ensemble de caractères, plus aucune ligne ne
correspond, et la Grille apparaît vide sans le moindre message. Ajout de
`version: 1` + `migrate`, plus une garde dans le filtre pour ne jamais
redevenir muet sur un état inattendu.
2. STOCK « tous magasins » incomplet. Le stock est un NIVEAU, pas un flux : un
magasin sans mouvement dans le mois détient toujours sa marchandise. Or le
TOTAL était sommé depuis les lignes mensuelles brutes, donc n'incluait que
les sites ayant bougé ce mois-là — sur un produit à faible rotation, il
n'affichait que Frouard. Il est désormais recalculé depuis les séries par
site, qui sont reportées d'un mois sur l'autre.
3. CHANGEMENT DE MAGASIN qui se fige. Hors « tous magasins », un rattrapage
interroge l'API FF à raison d'UNE requête HTTP par article : sur un gros
fournisseur, cela fait des milliers d'appels. Borné à 300 (réglable via
GRID_STORE_RECONCILE_MAX), avec un avertissement explicite sur ce qui n'a
pas été rattrapé — pas de troncature silencieuse.
Enfin, getProductRows ne renvoie plus [] en cas d'erreur : une liste vide est
indiscernable d'un fournisseur sans article. La Grille affichait une page
blanche sans explication, et la synchro nocturne prenait la panne pour un
fournisseur vide — qu'elle désactivait automatiquement. L'erreur remonte
désormais jusqu'au bandeau rouge et au statut « echec ».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
|
||
|
|
720cf3c7a1 |
feat(grille): filtre nomenclature en multi-sélection
Le filtre était un <select> à valeur unique : impossible de retenir plusieurs nomenclatures, ni de partir de tout pour retirer les postes non voulus — ce qui est la manière naturelle de travailler quand un fournisseur en compte des dizaines. Remplacé par une liste déroulante à cases, avec recherche, « Tout cocher », « Tout décocher », « Ne garder que ces N » sur le résultat d'une recherche, et une croix pour tout réafficher. Décocher depuis l'état « tout » matérialise la liste complète puis en retire le poste, sinon le premier décochage n'aurait rien fait. filters.code3 passe de `string | null` à `string[] | null` : `null` = aucun filtre, `[]` = rien de coché. Distinguer les deux évite la valeur sentinelle qu'imposerait un simple tableau, et rend « Tout décocher » sans ambiguïté. L'URL accepte une liste séparée par des virgules. Au passage : la Grille rechargeait tout depuis le serveur à chaque changement de nomenclature, alors que getProductRows ignore ce filtre et que le tri se fait en local. Le paramètre n'est plus transmis ni dans les dépendances de l'effet — le filtrage est désormais instantané, ce qui compte d'autant plus avec plusieurs cases à cocher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
0bd7ec9798 |
feat(admin): synchronisation nocturne des fournisseurs, paramétrable
Page /admin/synchronisation : choisir quels fournisseurs sont cadencés, pour quelle source, et suivre l'avancement. ORDRE DE PASSAGE. La file est « le plus anciennement synchronisé d'abord », en ignorant ceux déjà traités depuis l'ouverture de la fenêtre. Il en découle, sans curseur ni compteur de tour : la nuit 2 reprend là où la nuit 1 s'est arrêtée, le tour recommence quand tout le monde est passé, et un fournisseur en échec garde une date ancienne donc repasse en tête. DEUX FILES. SQL (~43 s sur le plus gros) et Qlik (jusqu'à 8 min, sur un serveur qui abandonne quand on le sollicite trop) ont leur propre activation et leur propre rythme. Une extraction Qlik est intercalée tous les N fournisseurs SQL, sinon elle ne démarrerait jamais avant que la file SQL soit vide. Un plafond par nuit et un âge minimal évitent de refaire chaque nuit des agrégats mensuels. Détails qui comptent : - La fenêtre signifie « ne plus DÉMARRER après l'heure de fin » : une extraction en cours n'est jamais coupée. Le passage par minuit est géré. - L'horodatage est écrit même en cas d'échec, sans quoi le fournisseur fautif bloquerait la file derrière lui. - Garde anti-boucle : l'avancement repose sur cette écriture, volontairement non bloquante ; si elle échoue, le round s'arrête au lieu de reprendre le même fournisseur indéfiniment. - Un fournisseur sans article est désactivé automatiquement, motif affiché, réactivable — plutôt que réessayé chaque nuit. - Désactivé par défaut : un déploiement ne doit pas se mettre à solliciter Qlik la nuit suivante sans décision explicite. Déclenchement par minuteur interne (instrumentation.ts), battement d'une minute. Aucune configuration côté hôte, se réarme au démarrage ; en contrepartie rien ne tourne si l'application est arrêtée. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
be8d576af5 |
chore: supprime les derniers restes de l'IA dans les Paramètres
La section « IA Copilot — OpenRouter » annonçait « pour les analyses de gammes », mais aucun code ne consommait cette clé : elle était décorative. Supprimée, ainsi que les routes devenues orphelines /api/admin/ai-config, /api/google-ai/models et /api/openrouter/models. Nettoyage de l'état correspondant dans la page : clé, liste de modèles, fournisseur d'IA et modèle Google ne faisaient plus qu'un aller-retour entre le chargement et l'enregistrement, sans aucune interface pour les modifier. handleSave n'enregistre donc plus que l'URL de la base. La règle set-state-in-effect s'est mise à signaler le `setIsMounted` du montage, inchangé mais devenu analysable une fois le code IA retiré : dérogation locale, comme dans api-connection-info.tsx, puisque poser l'indicateur au montage est précisément ce qui évite l'écart d'hydratation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
fff4cd9641 |
chore: retire la section Score des Paramètres et supprime la page Chat IA
- Section « Score Produit » retirée : elle ne faisait que décrire une formule, sans rien paramétrer. - Page /admin/ai-chat et sa route /api/admin/ai-chat supprimées, ainsi que l'entrée de menu et la section « Admin AI Chat — Fournisseur » des Paramètres, qui n'existait que pour la configurer. - Nettoyage de l'état devenu mort dans la page Paramètres : la liste des modèles Google et son chargement n'avaient plus d'interface. Effet de bord notable : tsc passe de 23 à 4 erreurs préexistantes — la page de chat en portait 19 à elle seule (flags de regex exigeant une cible es2018). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
310165151d |
fix(qlik): reprise globale quand le moteur abandonne l'extraction (code 15)
Deux exécutions du même fournisseur D005, même code, même jour : 04:17 → OpenDoc en 78 s, succès en 479 s 06:13 → OpenDoc en 233 s, « code 15 / Request aborted » après 333 s Le moteur Qlik est trois fois plus lent à 6 h qu'à 4 h et finit par abandonner la requête. Les reprises par requête de rpcWithRetry ne servent à rien ici : c'est toute l'extraction qui tombe, et la sync repartait à zéro sans réessayer. L'extraction est désormais relancée entièrement sur erreur transitoire (code 15 ou « Request aborted »), une fois par défaut, après une pause de 60 s — pas immédiatement : relancer aussitôt ne ferait qu'ajouter de la charge au serveur qui vient de renoncer. Réglable via QLIK_EXTRACTION_RETRIES et QLIK_EXTRACTION_RETRY_PAUSE_MS. Le reste du comportement est préservé : le refus de publier une fenêtre 12 mois incomplète, et la reprise sur point de contrôle hors fenêtre datée, s'appliquent comme avant une fois les reprises épuisées. Le checkpoint est vidé entre deux tentatives pour ne pas mélanger deux extractions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
0a57a9a707 |
fix(api): nomenclatures dérivées du préfixe de code3, pas de code1/code2
Les codes de nomenclature FF font 6 chiffres et sont hiérarchiques par préfixe (31xxxx à 40xxxx) : « 32 » univers, « 3202 » famille, « 320211 » sous-famille. L'endpoint groupait sur les colonnes code1/code2, qui ne sont renseignées que si pgGetNomenclatureByFournisseur a su remonter la hiérarchie — ce qui dépend d'une détection heuristique de la colonne parent de la table `nomenclature`. Quand elle échoue, seul code3 est rempli et l'endpoint aurait renvoyé une liste vide aux niveaux 1 et 2, sans rien signaler. Les niveaux sont désormais dérivés de left(code3, 2|4|6), toujours disponible. En conséquence, /grid gagne un filtre `nomenclature` par préfixe, qui fonctionne aux trois niveaux et ne dépend pas non plus de code1/code2 : c'est lui que meta.filtreGrid désigne. Les filtres code1..code3 exacts restent inchangés. Les libellés viennent toujours du payload et peuvent être null aux niveaux 1 et 2 pour la même raison ; codes et compteurs, eux, sont exacts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
60aa2ce120 |
feat(api): inventaire des nomenclatures, pour découper un gros fournisseur
Les filtres code1/code2/code3 existaient déjà sur /grid, mais un appelant ne pouvait pas savoir quelles valeurs existent chez un fournisseur : il aurait dû tout télécharger pour les découvrir, ce qui annulait l'intérêt du découpage. GET /api/v1/nomenclatures?fournisseur=…&niveau=1|2|3&parent=… renvoie les postes avec leur nombre d'articles et leur CA, agrégés en SQL — quelques dizaines de lignes, même pour un fournisseur de 130 000 articles. `parent` permet de descendre l'arborescence sans tout charger, et meta.filtreGrid indique le paramètre de /grid correspondant au niveau demandé. Le parcours devient : lister les postes, puis /grid?fournisseur=…&code1=… poste par poste. Bien plus praticable pour une IA que 132 000 lignes d'un bloc, et plus pertinent métier que le découpage par gamme. Les libellés sont lus dans le payload jsonb (ils n'ont pas de colonne dédiée). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
8fe294924b |
fix(api): tient les très gros fournisseurs (132k articles)
Le journal du fournisseur D005 montre 132 388 lignes persistées pour un seul
fournisseur. À cette échelle, /api/v1/grid sans limit ne tenait pas.
Deux plantages francs, indépendants de la taille de la réponse :
- getNetworkMetricsByCodeCentrale : inArray avec 132 000 valeurs, soit autant
de paramètres liés, alors que le protocole PostgreSQL en accepte 65535.
- pgGetGammesByCodeins : même problème sur son IN (…).
Les deux lectures sont désormais découpées par lots de 2000. Le bug existait
pour tout appel dépassant ~65 000 codes, indépendamment de l'API.
Volume : charger 132k lignes complètes puis les sérialiser d'un bloc représente
plusieurs centaines de Mo en mémoire. Sans `limit`, la réponse est maintenant
diffusée en flux, par lots internes de 1000, à mémoire constante. La forme
{ data, pagination, meta } est inchangée — un client existant ne voit que des
octets arrivant progressivement.
Une erreur survenant après les premiers octets ne peut plus changer le code
HTTP : le document est alors fermé avec meta.erreur et meta.complet=false, pour
qu'une réponse tronquée ne passe pas pour complète.
`fields` reste le levier décisif à cette échelle : une ligne complète porte les
séries mensuelles et les ventilations par magasin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
|
||
|
|
33b30dc8bd |
fix(api): supprime le plafond de 500 lignes, qui tronquait en silence
Le passage à limit=5000 du commit précédent était sans effet : queryGridRows re-plafonnait à 500 en dur (Math.min(500, …)). Pire, la pagination était calculée sur le limit *demandé*, donc hasMore et meta.complet annonçaient une réponse complète alors qu'elle était tronquée — exactement la troncature silencieuse que ces champs devaient éliminer. - Plus aucun plafond. Sur /grid, omettre `limit` renvoie toutes les lignes du fournisseur : plus de pagination à dérouler, ce que les agents font mal. - La pagination reflète le mode sans limite (une page couvrant le total), donc hasMore et meta.complet disent la vérité. - Le défaut reste 100 sur les endpoints de recherche, non bornés par un fournisseur — mais sans maximum imposé. - meta.avertissement explique désormais que la troncature vient du `limit` fourni, et qu'il suffit de l'omettre. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
73accd6d60 |
feat(api): un seul appel suffit pour tout un fournisseur
Le plafond de pagination était de 500 lignes. Un lot fournisseur dépasse souvent ce seuil, si bien qu'une app ou un agent ne récupérait qu'une partie des données sans forcément s'en rendre compte — et les agents paginent mal, concluant volontiers qu'ils ont tout lu. - /grid accepte désormais limit jusqu'à 5000. La borne reste à 500 sur la recherche transversale, qui n'est pas bornée par un fournisseur. - pagination.hasMore, et meta.complet sur /grid : l'appelant sait s'il a tout, sans avoir à comparer page et totalPages. - Réponse partielle : meta.avertissement indique le nombre de lignes obtenues sur le total et comment obtenir le reste. Pas de troncature silencieuse. - Doc des Paramètres et openapi.json mis à jour, avec un exemple curl qui récupère un fournisseur entier. `fields` reste vivement conseillé : une ligne complète porte les séries mensuelles et les ventilations par magasin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
3fa92e891b |
merge: intègre main — main prioritaire, abandon de la page en doublon
main a beaucoup avancé (53 commits) pendant que cette branche construisait l'API. La fusion a révélé que /produits, sur main, fait la même chose que la page Recherche réseau d'ici, en plus abouti : fiche produit, opportunités, URL partageables, et sa propre chaîne Qlik (lib/qlik-search, pgSearchProduits). Résolution, main prioritaire : - sidebar, heatmap-grid, qlik-playwright : version de main telle quelle. Son travail sur la tendance et Qlik est postérieur et plus complet. - get-product-rows : import NB_MAGASINS_RESEAU de main, plus l'appel upsertGridRows d'ici — sans lui /api/v1 n'aurait aucune donnée à servir. - settings : les deux côtés (ServerLogs de main + les sections API d'ici). Suppression du doublon : page recherche-reseau, /api/qlik/search, ses types et composants. searchNetworkProductsPlaywright disparaît avec la version main de qlik-playwright ; rien d'autre ne l'utilisait. api-enrich se branche désormais sur le module de tendance de main (features/grid/lib/network-trend) plutôt que sur le mien, supprimé : sa mesure est meilleure — variation sur la quantité par magasin vendeur, ce qui distingue « vend mieux » de « est distribué plus largement » — et l'API renvoie ainsi exactement ce qu'affiche la Grille. Le champ trend gagne surQteParMagasin et nouveau. Apport conservé de la branche : l'API /api/v1, absente de main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
94e82aa404 |
fix(ff): le panneau API FF Nancy affichait une coquille vide + URL configurable
Deux problèmes distincts, dont un mal diagnostiqué au départ.
1. FORMAT DE RÉPONSE — cause réelle du « test ne donne rien ». Le serveur
fonctionne (vérifié : HTTP 200, synchro du jour, 33 tables), mais il renvoie
{ sync: [{ table_name, last_sync, rows_synced, status, error_msg }] } alors
que l'application lisait { lastSync, tables: [{ nom, derniereSync, nbLignes }] }.
Les champs n'existaient pas : le panneau s'affichait vide sans erreur, la
requête HTTP ayant réussi. normalizeSyncStatus() traduit désormais les deux
formes, et le tableau expose l'état par table (une table en erreur est
justement l'information qu'on vient chercher).
2. URL NON CONFIGURABLE. FF_API_BASE était figée au chargement du module depuis
process.env, sans champ dans l'interface : impossible de corriger l'adresse
sans redéployer. Elle est maintenant résolue à l'appel — réglage enregistré,
puis variable d'environnement, puis défaut — avec un champ, un bouton
Enregistrer et un test dans Paramètres.
Le test distingue désormais DNS, connexion refusée, délai dépassé et code HTTP,
et affiche l'adresse réellement appelée : un « HTTP 503 » nu ne permettait pas
de séparer une mauvaise URL d'une panne. /api/ff-status renvoie le même détail.
Ajout au passage du paramètre `compute` dans la doc API des Paramètres.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
|
||
|
|
18edda7167 |
feat(api): expose la tendance réseau calculée dans /api/v1
La colonne « Tendance » de la Grille était la seule information visible que
l'API ne fournissait pas : elle est calculée dans le navigateur par
computeNetworkTrend(), qui vivait dans un composant "use client" donc
inaccessible côté serveur. L'API livrait la série mensuelle brute et laissait
chaque appelant refaire la régression — au risque de diverger de l'affichage.
- Le calcul part dans src/lib/network-trend.ts (module pur, sans React) ;
le composant le réexporte, aucun import existant ne change.
- enrichRows ajoute `trend` : { direction, pct, label, monthsUsed }, calculé
sur la même série que la colonne de l'application.
- openapi.json : schéma Trend documenté, et il est dit explicitement que la
liste des propriétés de Product n'est pas exhaustive — la réponse porte la
ligne de grille entière.
Vérifié au passage que rien n'est perdu en chemin : upsertGridRows stocke le
ProductRow complet dans la colonne jsonb `payload` (les colonnes scalaires ne
servent qu'aux index), ne filtre que les lignes sans codein, et pickFields
renvoie tout tant que `fields` n'est pas précisé.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
|
||
|
|
3a825866d2 |
feat(api): /api/v1/grid calcule le fournisseur à la demande
L'API lisait uniquement la table grid_rows, écrite en effet de bord quand un
humain ouvre la page Grille. Un fournisseur jamais ouvert renvoyait donc
202 not_ready : une app ou un agent externe ne pouvait consulter que ce qui
avait déjà été parcouru dans l'interface, ce qui vide l'API de son intérêt.
/api/v1/grid déclenche désormais getProductRows() quand l'instantané manque.
Le calcul est déjà protégé en amont (cache 10 min + verrou anti-concurrence)
et persiste l'instantané, donc seul le premier appel paie le coût ; les
suivants repassent par le chemin SQL rapide.
- compute=1 (défaut) : calcul à la demande, meta.computedOnDemand signale
quand il a eu lieu. compute=0 : comportement strict d'avant.
- Fournisseur inconnu ou sans article : 404 explicite, distinct d'une panne
(500) et de « pas encore calculé » (202).
- /api/v1/products/{codein} fait de même lorsque `fournisseur` est fourni,
le calcul se faisant par lot fournisseur.
- openapi.json mis à jour : c'est ce document que lit un agent externe, il
annonçait « aucun endpoint ne déclenche de recalcul ».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
|
||
|
|
54d51e8eab |
fix(reseau): cast texte sur artcentrale pour la jointure code centrale
Le type réel de articles.artcentrale varie selon l'installation FF Nancy (varchar ou numérique). La comparaison avec des codes centraux (chaînes) passe désormais par TRIM(...::text), comme ailleurs dans ce fichier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
c565672259 |
feat(reseau): page Recherche réseau — chercher dans le catalogue Qlik
La Grille part de NOTRE catalogue : on extrait nos codes centraux puis on demande à Qlik les métriques de ces codes. Un produit vendu par le réseau mais absent de FF Nancy était donc invisible par construction. Cette page inverse le sens : elle interroge Qlik d'abord, par libellé ou par code centrale, et affiche aussi les produits que nous ne référençons pas. - searchNetworkProductsPlaywright() : sélection par SelectValues sur « Article Code » (mode code) ou par SearchListObjectFor + AcceptListObjectSearch sur la dimension « Article » (mode plein texte, avec repli sur des noms de champs si la master dimension échoue). Cube [Article Code, Article, Mois] × 6 mesures, passe N-1 incluse pour obtenir 12 mois glissants contigus. Garde-fou à 300 produits/recherche. - /api/qlik/search : job asynchrone (POST + polling GET) comme la sync, une seule recherche à la fois pour ménager le moteur Qlik. Met les résultats en cache dans qlik_network_metrics et enrichit avec le catalogue local (badge « Chez moi » / « Réseau uniquement », stock). - Page /recherche-reseau : tableau triable, filtres, fiche produit avec la courbe 12 mois, export Excel. - Composants de tendance extraits de heatmap-grid vers features/network pour être partagés (comportement inchangé). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
4b229acc9b |
feat(api): API prête pour une IA externe type ChatGPT
Adapte l'API CollectFlow aux exigences des Actions ChatGPT et garantit que les données soient réellement disponibles pour un consommateur externe. Schéma OpenAPI : - openapi.json devient PUBLIC (sans clé). Il ne décrit que la structure, sans aucune donnée, or ChatGPT importe le schéma par URL avant que la clé ne soit configurée : l'exiger rendait l'import impossible. - servers[0].url est désormais ABSOLUE (une URL relative est rejetée à l'import), construite depuis l'hôte appelant ou COLLECTFLOW_PUBLIC_URL. - operationId sur chaque opération (requis par les Actions), schémas de réponse typés, et descriptions rédigées pour le modèle : quelle gamme utiliser pour raisonner, pourquoi préférer caParMagasinReseau au CA brut, que faire d'un 202 not_ready. Disponibilité des données : - Nouveau préchauffage /api/admin/grid-warmup + bouton dans Paramètres. Sans lui, l'API ne sert que les fournisseurs déjà ouverts à la main dans la Grille — une IA externe n'aurait presque rien vu. Le job calcule tous les fournisseurs séquentiellement (paralléliser saturerait PostgreSQL), saute ceux à jour depuis moins de 24 h et suit son avancement. Documentation : - Paramètres → marche à suivre pas à pas pour brancher un GPT (import du schéma, auth par clé personnalisée X-API-Key), et mention de COLLECTFLOW_PUBLIC_URL quand le domaine public diffère. L'assistant interne et son API api.ffnancy.fr ne sont pas touchés. Vérifié sur PostgreSQL local : schéma servi sans clé en 200, URL absolue, 5 operationId, auth apiKey/X-API-Key, surcharge COLLECTFLOW_PUBLIC_URL effective, 5 endpoints en 200 avec la clé, 401 JSON sans clé, et recherche transversale renvoyant bien plusieurs fournisseurs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
a45e09777b |
feat(api): métriques Qlik + gamme serveur à jour, et doc de connexion dans Paramètres
L'instantané grid_rows fige la ligne au moment du calcul, mais deux données évoluent indépendamment et doivent refléter l'état courant : - Métriques réseau Qlik : qlik_network_metrics est alimentée par les syncs et les recherches réseau, hors du calcul de grille. Relues à chaque appel et exposées dans `network` (null quand le produit n'en a pas). Les colonnes caReseau/qteReseau/... de la ligne sont réalignées dessus. - Gamme serveur : codeGamme peut être surchargée par un snapshot de session. L'API expose `codeGammeServeur`, la gamme NON modifiée telle qu'elle est en base PostgreSQL, relue à chaque appel. codeGammeInit est gardé aligné. Ajout de pgGetGammesByCodeins() : variante sans jointure artfou1, nécessaire car la recherche de l'API est transversale (pas de fournisseur connu). Ce n'est pas un recalcul : deux lectures indexées bornées à la page courante (500 lignes max). Mesuré à ~20 ms, soit le même coût que sans enrichissement. Paramètre enrich=0 pour servir l'instantané brut. Paramètres → nouvelle section « API CollectFlow — Connexion » : URL de base déduite de l'origine, en-têtes d'authentification, liste des endpoints et des paramètres, exemples curl copiables, lien vers openapi.json. Vérifié sur PostgreSQL local avec un instantané volontairement périmé : gamme snapshot A → serveur C, caReseau 111 → 990000, produit sans données réseau → network null, produit sans gamme → codeGammeServeur null, enrich=0 redonnant bien les valeurs figées, et toujours zéro recalcul. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
e79d7e51da |
feat(api): API CollectFlow /api/v1 — lecture de la grille et recherche
Les données de la grille (ventes 12 mois, stock, marges, gammes, métriques réseau Qlik) n'étaient accessibles par aucun moyen programmatique, et getProductRows() exige un fournisseur : chercher un produit sans le connaître était impossible. Contrainte de conception : l'API ne recalcule jamais rien. La grille était reconstruite en direct et gardée seulement 10 min en mémoire ; une API qui appellerait getProductRows() serait lente et imprévisible. On persiste donc le résultat d'un calcul qui a déjà lieu, et on le sert. - Table grid_rows : colonnes scalaires (filtre/tri/recherche en SQL) + payload jsonb du ProductRow complet. Remplie en effet de bord NON bloquant par getProductRows(), purge des articles disparus via computed_at. Survit aux redémarrages, contrairement au cache mémoire. - Endpoints /api/v1 : fournisseurs, grid, products/search (transversale, tous fournisseurs), products/:codein, network/:codeCentrale, openapi.json. Pagination, tri sur liste blanche, projection de champs, validation zod. 202 not_ready si un fournisseur n'a pas encore d'instantané. - Authentification double : clé d'API (X-API-Key ou Bearer, SHA-256 en base, révocable) ou session existante. Le middleware exempte /api/v1 — sans quoi un script recevait une redirection 302 vers /login au lieu d'un 401 JSON. - Gestion des clés dans /settings (server actions, clé affichée une seule fois). Vérifié contre une base PostgreSQL locale : 401 JSON sans clé, 401 sur clé révoquée, recherche renvoyant plusieurs fournisseurs, upsert + purge, et 24 appels /api/v1 sans déclencher un seul recalcul (l'ancienne route /api/grid/rows en déclenche un à chaque appel). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
150baeef9a |
Carte tendance : hauteur figee, bascule Graphique/Tableau, cellules copiables
1. LA CARTE CHANGEAIT DE TAILLE PENDANT LE SURVOL.
La lecture du mois vivait sous le graphique, en HTML : selon la longueur
des valeurs elle passait sur deux lignes et la carte sautait. Elle est
desormais dessinee DANS le SVG, dont la geometrie est figee par le
viewBox — infobulle ancree au reticule, qui bascule a gauche quand elle
deborderait a droite.
Deux causes residuelles trouvees a la mesure, pas a la lecture :
- le SVG etait dimensionne en pourcentage, donc sa hauteur dependait de
la largeur : il porte maintenant une hauteur en pixels ;
- un SVG en ligne traine l'espace sous la ligne de base (6 px mesures),
ce qui decalait la vue tableau : `display:block`.
Verifie en pilotant le composant reellement monte : 382,5 px au repos,
sur quatre positions de survol et dans les deux vues, en clair comme en
sombre.
2. LES VALEURS MOIS PAR MOIS SANS AGRANDIR LA CARTE.
Le deplaint aurait fait grandir la carte a l'ouverture — exactement ce
qu'on venait de corriger. Le tableau occupe donc la place du graphique
dans un panneau de MEME HAUTEUR et defile a l'interieur.
3. REFERENCE ET EAN COPIABLES, comme le code interne.
La copie etait ecrite en ligne dans la colonne « Code interne », avec un
`eslint-disable react-hooks/rules-of-hooks` : un useState dans le corps
d'un cell() de TanStack n'est pas rendu au meme endroit d'un rendu a
l'autre. Extraite en composant, la derogation disparait et les trois
colonnes partagent le meme comportement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
|
||
|
|
5ad222431d |
Carte tendance : refonte du graphique selon les regles data-viz
Petits multiples empiles sur un axe des mois commun, palette validee par script, etiquetage selectif, survol et vue tableau. FORME. Trois mesures d'ordres de grandeur differents (qte/magasin, volumes, magasins) ne peuvent pas partager un cadre : il faudrait deux ou trois echelles verticales dont l'alignement serait arbitraire, ce qui inventerait des correlations absentes des donnees. Trois facettes, une echelle chacune, le meme axe horizontal. La facette PRINCIPALE est la qte/magasin. COULEUR. Slots 1-3 de la palette categorielle de reference, en variables CSS, avec un jeu de pas CHOISI pour le fond sombre (pas un eclaircissement automatique). Palette passee au validateur dans les deux modes, toutes paires : bande de clarte, plancher de chroma, separation daltonienne (pire paire ΔE 9,2 clair / 9,4 sombre) et contraste. En mode clair l'aqua tombe a 2,82:1 — la regle de relief s'applique, d'ou etiquettes visibles ET vue tableau, toutes deux presentes. La teinte de tendance (vert/rouge/gris) ne vit plus que dans la tuile de verdict, avec fleche et libelle : un etat ne se signale jamais par la couleur seule, et la meme couleur ne veut pas dire deux choses. LISIBILITE. Etiquetage selectif — l'extreme et le dernier mois, pas les 36 valeurs. Le texte ne porte jamais la couleur d'une serie (illisible en teinte claire) : l'identite vient du repere pose a cote. Filets de base pleins et discrets, jamais pointilles. Reticule au survol reliant les trois facettes au meme mois, et vue tableau depliable : aucune valeur n'est accessible au seul survol. En-tete refait en tuiles : verdict, qte/magasin du dernier mois, magasins vendeurs. Chiffres proportionnels sur les valeurs mises en avant, tabulaires seulement dans le tableau ou les colonnes s'alignent. Rendu verifie en image dans les deux themes avant livraison : la version precedente faisait passer l'etiquette du pic par-dessus le titre de la facette — elle bascule desormais sous le point quand elle toucherait le bandeau. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
eaac945345 |
Tendance : la calculer sur la QTE PAR MAGASIN, pas sur le volume brut
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 |
||
|
|
e6e79dfe06 |
Carte tendance : desencombrer le graphique, il etait illisible
La densite etait le vrai probleme. La version precedente imprimait les 24 valeurs (12 quantites + 12 magasins) et titrait chaque bande DANS le SVG. Resultat sur ecran : les titres passaient par-dessus les courbes, les nombres des extremites etaient coupes par le bord du cadre, et les etiquettes des magasins se collaient au trait pointille. - Les titres sortent du dessin : legende HTML au-dessus, plus aucun recouvrement possible quelle que soit la forme des courbes. - La bande des magasins ne porte plus que sa DERNIERE valeur ; son niveau se lit a la forme et au maximum rappele dans la legende. - Les etiquettes des bords sont ancrees vers l'interieur (start / end au lieu de middle) : plus rien n'est tronque. - Chaque mois expose une infobulle native avec les valeurs exactes, ce qui rend inutile d'imprimer tous les nombres. - Cadre elargi (520 de large, marge haute pour les etiquettes du sommet) et modale passee en max-w-xl. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
bf6fef6a14 |
Carte tendance : deux bandes separees, indicateur lisible, courbe magasins reparee
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
|
||
|
|
28fe78a646 |
Carte tendance : garantir que la courbe magasins se distingue des quantites
La courbe des quantites prend la couleur de la TENDANCE : vert, rouge, ou gris-bleu (#94a3b8) quand elle est stable. L'indigo des magasins se detache du vert et du rouge, mais il est trop proche du gris-bleu — or « stable » est le cas le plus frequent. storesColorFor() bascule sur l'ambre (#f59e0b) dans ce cas precis : gris froid contre orange chaud, impossible a confondre. Les deux series se distinguent desormais par TROIS canaux, pas seulement la couleur — qui ne suffit ni en impression noir et blanc, ni pour un daltonien : - teinte garantie differente, - trait pointille contre trait plein, - points evides contre points pleins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
abc704a363 |
Carte tendance : deuxieme courbe du nombre de magasins vendeurs
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 |
||
|
|
b6225f1920 |
Qlik: la sync 12 mois passe — sonde fournisseur bornee, commentaires rectifies
Premier run reussi : 12 mois couverts, 40 411 produits ecrits dans le
cache, statut final=success.
Ce que la sonde paginee montre enfin :
Periode « (aucune) » → master exposee : 2024-01…2026-07 (31 mois)
Sans aucune Periode selectionnee, « Quantite N » couvre 31 mois, et une
seule passe (Annee=2026) sert les 12 mois de la fenetre. L'etat libre
etait donc le bon depuis le debut ; ce sont deux defauts de l'outil de
MESURE qui le cachaient — la sonde tronquee a 60 lignes, puis le Clear
appele sur le list object. Le commentaire de choisirPeriode affirmait
l'inverse (« sans Periode la master est morte ») : il est rectifie, avec
la mesure a l'appui.
Sonde fournisseur : elle lisait 1 207 205 codes en 240 pages avant de
conclure, trois fois de suite — 87 s pour un rejet joue d'avance. La
lecture s'arrete desormais des le plafond depasse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
|
||
|
|
fee267004f |
Qlik: Clear/SelectValues sur le CHAMP Periode, pas sur un objet de session
Le journal complet (enfin lisible) donne la cause, et elle est unique :
choix de « Periode » impossible :
{"code":-32601,"parameter":"Clear","message":"Method not found"}
extraction par date brute interrompue :
{"code":-32601,"parameter":"Clear","message":"Method not found"}
Clear et SelectValues sont des methodes de CHAMP. Je les appelais sur le
list object « Periode ». Consequence : la cartographie des periodes ET le
chemin d'extraction par date brute n'ont jamais tourne — d'ou la
couverture master annoncee a 0/12 alors que la master vit (total mesure :
11 591 263).
- Handle du champ « Periode » obtenu comme ceux d'Annee et de Date ; le
list object ne sert plus qu'a ENUMERER les valeurs.
- appliquerPeriode selectionne par texte et ne propage plus d'erreur :
une periode qu'on n'arrive pas a poser doit degrader le resultat, pas
interrompre l'extraction.
- Chaque candidat de la cartographie est isole : une valeur en echec est
notee et ignoree, elle n'emporte plus toute la carte.
Sonde fournisseur : « J009 » laissait 1 207 205 articles visibles, soit
tout le catalogue — la selection ne restreignait rien, et la
« couverture » de 97 % ne faisait que constater que le catalogue contient
nos codes. Une selection qui laisse plus de 3x les codes demandes est
desormais rejetee immediatement (elle coutait 3 x 24 s de lecture).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
|
||
|
|
d89d2b3232 |
Parametres : telechargement des journaux serveur complets
Une extraction Qlik produit plusieurs milliers de lignes de diagnostic. Un terminal les tronque et n'en laisse que la fin, alors que l'information decisive — carte du modele Qlik, champs date retenus, choix des periodes — se trouve en TETE. Diagnostiquer sur la fin du log revient a travailler a l'aveugle. - src/lib/log-capture.ts : instrumente console.log/warn/error une fois, recopie chaque ligne horodatee dans les captures ouvertes, ecrit au fil de l'eau dans le repertoire temporaire (un job qui fait tomber le process laisse quand meme son journal). Bornes : 200 000 lignes, 8 000 caracteres par ligne, 20 fichiers conserves. - GET /api/logs : liste JSON ; GET /api/logs?id=<jobId> : le fichier complet en piece jointe. Admin uniquement. - La sync Qlik ouvre la capture AVANT son premier log et la ferme dans un finally, avec le statut final — les deux modes (fournisseur et produit). - Parametres : section « Journaux serveur », un bouton Telecharger par job, le plus recent en premier, badge « en cours » pour un job vivant. Aucun secret ne transite dans ces journaux : l'extracteur ne journalise que le NOM du cookie de session, jamais sa valeur ni le mot de passe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
50f883ec98 |
Qlik: extraction mois par mois sur le champ date brut, sans pont de periodes
Le log livre le fait qui manquait :
dimension « Mois » → 35602068 (100% du total sans filtre)
100 %, donc TOUTE la table de faits tient dans les 12 mois. Le test de
champ date « il doit faire baisser le total » etait donc structurellement
faux : n'importe quel champ correct donne 100 % quand la fenetre couvre
tout. Il rejetait les bons champs, ce qui condamnait le seul chemin sain
et renvoyait vers la dimension Mois — celle qui demultiplie les faits.
Nouveau chemin moisParDateBrute(), essaye AVANT l'agregation par
dimension :
- Type_Cal / Periode / Annee effaces : on veut les faits nus, pas un
contexte de calendrier. Plus aucun mois « inatteignable », puisqu'un
mois y est un filtre de dates.
- detection du champ date sur UN SEUL MOIS : valide si le total est non
nul ET une fraction du total (ni 0, ni 100 %).
- cube [Article Code] x 4 expressions, SANS dimension Mois : c'est elle
qui passe par le pont et rattache un fait a plusieurs contextes
(mesure : x5,3).
- 12 selections de dates, 12 cubes pagines, un point de controle par
mois.
Controle d'integrite : chaque mois doit etre servi, et la somme des 12
mois ne doit pas depasser le total sans filtre de date — une
demultiplication ferait exploser ce rapport. Sinon rien n'est ecrit.
Corrige aussi le meme test defectueux dans moisParExpressions, et permet
d'effacer Periode meme si son list object n'a pas pu etre cree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
|
||
|
|
b948aca2f8 |
Qlik: une Periode PAR ANNEE, et sonde de couverture paginee
Deux defauts, l'un cachant l'autre. 1. La sonde de couverture ne lisait que les 60 premieres lignes du cube [Mois] x Quantite N. La dimension Mois porte tout l'historique de l'app : les mois recents tombaient hors de la premiere page et la sonde annoncait 0/12 pour TOUTES les valeurs de Periode — alors que la master vivait (mesure dans le meme log : total 9 721 153). C'est ce faux zero qui faisait effacer Periode et condamnait l'extraction. Le cube est desormais pagine integralement, et la sonde journalise aussi ce que la master expose hors fenetre. 2. La bonne valeur de Periode DEPEND DE L'ANNEE. « Annee a date » convient a 2026 (janvier -> mois courant) mais pas a 2025, dont on veut les 12 mois. Le choix global ne pouvait donc satisfaire que l'une des deux passes — ce que montrait le log : 2026 servait 2026-01..06, la passe 2025 ne servait rien. choisirPeriode cartographie maintenant, pour chaque annee de la fenetre, la valeur qui sert le plus de mois attendus de cette annee-la (l'etat sans Periode etant un candidat comme un autre), et chaque passe annuelle pose SA valeur avant de selectionner l'annee. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |