Commit Graph
496 Commits
Author SHA1 Message Date
Claude 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
2026-08-06 10:56:29 +00:00
Claude 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
2026-08-06 10:36:35 +00:00
Claude 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
2026-08-06 10:29:26 +00:00
Claude 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
2026-08-05 11:25:42 +00:00
Claude 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
2026-08-05 11:22:53 +00:00
Claude 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
2026-08-05 10:21:31 +00:00
Claude 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
2026-08-05 10:14:21 +00:00
Claude 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
2026-08-05 10:14:21 +00:00
Claude 03b6ddb8c9 fix(qlik): mesures résolues par titre + relevé étiqueté (vérifier qté vs nb magasins)
CA / Quantité / Nb magasins étaient pris depuis des qId d'environnement
(GUID), sans garantie qu'ils pointent la bonne mesure — les deux autres
mesures étaient déjà résolues par titre pour cette raison. Toutes le sont
désormais ('CA N', 'Quantité N', 'Magasin Ventes Nb N'), avec repli sur
l'env.

Ajout de deux logs de vérification :
- les identifiants de mesure réellement utilisés ;
- un échantillon de ligne mensuelle avec chaque valeur ÉTIQUETÉE, pour
  comparer directement avec Qlik et confirmer que la courbe utilise bien
  la quantité vendue et non le nombre de magasins.
2026-07-26 04:41:36 +00:00
Claude 59efd70886 feat(qlik): 12 mois glissants contigus via passe complémentaire année N-1
Le graphe sautait de 07/25 à 01/26 : l'astuce COMP ne fournit un mois N-1
que s'il existe un mois N correspondant. L'année N s'arrêtant au mois
courant (juillet), les mois août→décembre N-1 étaient introuvables.

Ajout d'une passe complémentaire : on sélectionne le champ 'Année' sur
N-1, ce qui fait porter les mesures 'N' sur cette année → ses 12 mois
sont récupérés directement (prioritaires sur les valeurs COMP dérivées).
Les totaux réseau (CA/Qté) ne sont alimentés que par la passe principale
pour éviter tout double comptage. Passe ignorée proprement si le champ
'Année' est absent ou si elle échoue.

Résultat attendu : mois contigus 2025-08 → 2026-07 dans le modal (l'UI
garde les 12 derniers mois triés).
2026-07-26 04:34:55 +00:00
Claude 99097b6eca fix(qlik): dérive les mois N-1 depuis Quantité COMP (vrais 12 mois)
Le cube [Article Code, Mois] ne renvoie que les mois de l'année N (les
mesures 'N' sont nulles ailleurs → lignes supprimées). Le code cherchait
des lignes d'année N-1 qui n'existent pas, donc 'Quantité COMP' n'était
jamais exploitée → seulement ~7 mois affichés.

'Quantité COMP' étant le comparable N-1, à la ligne Mois=YYYYMM elle donne
la quantité du MÊME mois l'année précédente. On dérive donc deux points
par ligne : YYYY-MM (Quantité N) et (YYYY-1)-MM (Quantité COMP) → 12 mois
sans ligne supplémentaire ni requête en plus.

UI : l'en-tête du modal affiche le nombre RÉEL de mois disponibles au lieu
de '12 derniers mois' en dur.
2026-07-25 21:31:15 +00:00
Claude 1633d5e407 fix(qlik-sync): message explicite quand le serveur Qlik est en OOM
Quand l'app Qlik ne peut pas se charger faute de RAM serveur (code 6
'Not enough memory to load file' / code 3002 'File corrupted' / 'Out of
memory'), le job affiche un message clair ('Serveur Qlik saturé…') au
lieu de l'erreur technique. Ce n'est pas un bug CollectFlow : le sync
échoue à l'ouverture de l'app, avant toute requête.
2026-07-24 08:11:58 +00:00
Claude 2af7c42b2b fix(grille): tooltip tendance (régression 12 mois) + lots Qlik plus petits (OOM)
- Corrige le tooltip erroné 'X% (4 derniers mois vs 4 précédents)' →
  'Tendance X% sur 12 mois (régression)'. Le calcul est déjà une
  régression linéaire sur 12 mois ; seul le texte était resté l'ancien.
- Réduit la taille des lots du cube [Article Code, Mois] (800→300, replis
  150/75) pour alléger la mémoire moteur Qlik et limiter les 'Out of
  memory / File corrupted' (3002) observés sur le sync.
2026-07-24 07:41:41 +00:00
Claude ac1c404ce5 feat(grille): modal courbe sur 12 mois glissants (réintègre Quantité COMP)
Le modal doit couvrir 12 mois glissants (pas seulement depuis janvier).
Comme 'Quantité N' ne couvre que l'année en cours, on réintègre la mesure
'Quantité COMP' (N-1) pour les mois de l'année précédente : mois année N
→ Quantité N, mois année N-1 → Quantité COMP. La courbe du modal affiche
donc les 12 derniers mois.
2026-07-24 06:29:37 +00:00
Claude 5b5e69eed1 feat(grille): modal en courbe unique + retrait Quantité COMP (2025)
- Modal : les 12 barres remplacées par UNE seule courbe SVG des ventes
  réseau mensuelles (points + quantités affichées + axe des mois).
- Sync : retrait de la mesure 'Quantité COMP' (année N-1) — trop lent à
  récupérer. Retour au cube [Article Code, Mois] à 5 mesures (Quantité N).
  Le détail mensuel couvre donc les mois de l'année en cours ; la tendance
  (régression) se calcule sur ces mois.
2026-07-23 20:40:24 +00:00
Claude 6d349e33b4 feat(grille): modal sur colonne Tendance + tendance par régression 12 mois + Quantité COMP
UI:
- La colonne Tendance (renommée 'Réseau · 12 m') ouvre le modal au clic
  (au lieu de la colonne Nb magasins, remise en simple affichage).
- Tendance calculée par RÉGRESSION LINÉAIRE (moindres carrés) sur les 12
  derniers mois : robuste au bruit, % = pente×durée/moyenne. Libellé
  Forte hausse/Hausse/Stable/Baisse/Forte baisse (seuils ±8% / ±25%).

Sync:
- Ajout de la mesure 'Quantité COMP' (année N-1) au cube [Article Code,
  Mois] → 12 mois glissants : mois de l'année N via Quantité N, mois de
  l'année N-1 via Quantité COMP. Cube 8 colonnes, PAGEM 1200 (<10000
  cellules). Total réseau reste en année N (non gonflé).
  Configurable : QLIK_MEAS_QTE_COMP_ID.
2026-07-23 19:53:20 +00:00
Claude 8a3f18f23b feat(qlik): découpage mensuel réel via dimension Mois (tendance + total corrigé)
La sonde a prouvé que les mesures 'Quantité N' se découpent correctement
par la dimension 'Mois' (id pfGAwTs, format 'YYYYMM'). L'itération par
sélection de Date, elle, renvoyait la valeur annuelle identique chaque
mois (tendance plate + total sommé 12×).

Nouvelle extraction réseau (activée par défaut, QLIK_MONTH_DIM=0 pour
revenir à l'ancienne) : un cube [Article Code, Mois] × 5 mesures par lot
de codes (avec repli code 15 800>400>200), sélection de la fenêtre de
dates une seule fois. Chaque ligne (code, mois, ...) alimente qteByMonth
en 'YYYY-MM' et le total par somme RÉELLE des mois.

Résultat attendu : la colonne Tendance affiche enfin de vraies variations,
le modal montre les ventes réseau mois par mois, et le total réseau n'est
plus gonflé 12×. Nécessite un nouveau Sync pour re-remplir le cache.

Note : 'Quantité N' = année N, donc les mois disponibles sont ceux de
l'année en cours (pas un glissant 12 mois strict) — suffisant pour la
tendance ; on pourra ajouter 'Quantité COMP' pour compléter N-1 si besoin.
2026-07-23 13:15:41 +00:00
Claude adb8455353 diag(qlik): déplace la sonde Mois à la fin du sync (visible dans la queue du log) 2026-07-23 12:10:49 +00:00
Claude 4cb2c7d1c5 diag(qlik): sonde Mois scopée (50 codes + fenêtre) pour éviter code 15 2026-07-23 12:05:21 +00:00
Claude 332b21c1ae diag(qlik): sonde cube [Article Code, Mois] x Quantite N
Le dump révèle une dimension 'Mois' (id pfGAwTs) et 'Période' (tKTPVU).
La sonde crée un mini-cube [Article Code, Mois] × Quantité N et logue le
format des valeurs Mois + si la quantité varie par mois — pour concevoir
le vrai découpage mensuel (dimension Mois) qui règlera tendance + total 12x.
2026-07-23 11:52:52 +00:00
Claude 473ed1e467 perf+diag(qlik): chemin rapide activé par défaut + dump mesures/dimensions
- Chemin rapide select-all activé par DÉFAUT (fiable, aucun code 15 en
  prod) — désactivable seulement via QLIK_SELECT_ALL_CODES=0. Plus besoin
  de la variable d'env (elle sautait à chaque mise à jour de la stack).
- Dump diagnostic des mesures + dimensions Qlik disponibles (titre+id)
  pour identifier une mesure Quantité non-annuelle / une dimension Mois,
  car les mesures 'CA N'/'Quantité N' renvoient la valeur annuelle figée
  (mois identiques → tendance impossible + total réseau sommé 12×).
2026-07-23 11:38:36 +00:00
Claude 74b183abb3 diag(qlik): distribution codes variants vs identiques (mois) + exemples 2026-07-23 11:35:27 +00:00
Claude 5b74133ecd diag(qlik): log échantillon qteByMonth (vérifie si les mois varient) 2026-07-23 11:14:58 +00:00
Claude 71563500a0 feat(grille): tendance réseau (sparkline) + modal ventes réseau par mois
Le sync Qlik calculait déjà les ventes réseau mois par mois mais les
sommait en un total. On conserve désormais le détail mensuel.

- Sync : accumulation additive qteByMonth { code: { 'YYYY-MM': qté } }
  in-page (chemins rapide + par lots), rattachée au métrique réseau.
  Reset propre en cas de repli code 15. Total inchangé.
- Stockage : colonne jsonb qte_by_month sur qlik_network_metrics
  (schema + db-init ALTER), upsert + lecture.
- ProductRow.qteReseauByMonth propagé jusqu'à la grille.
- UI grille :
  · colonne '% Présence réseau' remplacée par 'Tendance réseau' :
    sparkline 12 mois teintée + flèche + variation % (moyenne 4 derniers
    mois vs 4 précédents), triable ;
  · cellule 'Nb magasins' rendue cliquable → modal des ventes réseau
    mois par mois (barres, 12 derniers mois).

Nécessite un nouveau Sync Qlik pour peupler le détail mensuel (les
données déjà en cache n'ont que le total → modal 'pas encore de détail').
2026-07-22 14:17:57 +00:00
Claude 6c5e28b05e perf(qlik): chemin rapide select-all (sélection Article Code unique) derrière flag
La sync Qlik itérait mois × lots de 150 articles, en re-sélectionnant les
mêmes Article Code à chaque lot. La SelectValues répétée (~22 s × ~120)
représentait ~72% du temps total (~60 min pour 12 mois).

Ajoute un chemin rapide (flag env QLIK_SELECT_ALL_CODES=1) qui sélectionne
TOUS les Article Code une seule fois, puis n'itère que la sélection Date
par mois avec un seul cube paginé (Article Code en dimension). Passe de
~120 SelectValues Article Code à 1, et de ~120 cubes à 12 → gain estimé
~60 min → ~5-8 min.

Repli automatique et sûr : en cas d'erreur moteur code 15, les résultats
partiels et les sélections sont réinitialisés et l'ancien chemin par lots
reprend le relais. Flag désactivé par défaut : comportement inchangé tant
qu'on n'active pas la variable. À valider sur le Qlik réel.
2026-07-22 09:47:31 +00:00
Claude 2e25bbc5b3 feat(grille): bouton "Rafraîchir" pour forcer le rechargement serveur
Ajoute un bouton dans l'en-tête de la Grille qui force le rechargement
des données depuis le serveur en ignorant le cache mémoire de 10 min
(via le paramètre URL _refresh → refresh=1 → forceRefresh). Utile pour
remonter immédiatement l'état serveur courant, notamment la colonne INIT.
Spinner pendant le chargement, désactivé si un chargement est en cours.
2026-07-21 21:11:28 +00:00
Claude f0fdab465c fix(grille): colonne INIT rechargée à chaque chargement (même sur cache)
La colonne Gamme conserve la valeur du snapshot (comportement voulu). Mais
la colonne INIT ne se mettait pas à jour : getProductRows sert les lignes
depuis un cache mémoire de 10 min, et le client n'envoie 'refresh=1' que
sur le bouton de rafraîchissement manuel. Sur une navigation normale dans
les 10 min, codeGammeInit restait donc figé.

Correctif : sur un hit de cache, on re-requête la gamme serveur courante
(pgGetGammesByFournisseur, 1 requête légère) et on met à jour uniquement
codeGammeInit sur les lignes cachées. codeGamme (snapshot) n'est pas
touché ; les données lourdes (ventes/stock/réseau) restent cachées.

Résultat : la colonne INIT reflète toujours l'état serveur à chaque
chargement, et le marqueur 'modifié' (Gamme ≠ INIT) se résorbe dès que
le serveur rattrape la modification. La révision précédente (reconcilier
codeGamme) est annulée : la colonne Gamme reste le snapshot tel quel.
2026-07-21 15:05:02 +00:00
Claude e9ac5bc0a5 fix(grille): la grille reflète l'état serveur courant des gammes (INIT rechargé)
La grille rechargeait bien le gamme INIT depuis l'état live du serveur à
chaque chargement (Phase 6), mais la Phase 9 réappliquait ensuite
AVEUGLÉMENT le 'after' du dernier snapshot sur codeGamme — même quand la
modification avait déjà été appliquée sur le serveur (ou que la gamme
serveur avait évolué depuis). Résultat : la grille affichait l'ancienne
cible du snapshot au lieu de l'état serveur réel, et une modif 'en
attente' fantôme.

Désormais on ne réapplique une modif du snapshot QUE si elle est encore
réellement en attente côté serveur (change.before === gamme live). Sinon
on garde l'état serveur courant. La grille montre donc toujours où en
sont les gammes sur le serveur, et une modif disparaît du 'en attente'
dès qu'elle est appliquée côté serveur — le suivi reste juste.

Note : les brouillons locaux (localStorage) restent gérés côté client
comme du travail non sauvegardé.
2026-07-21 15:00:40 +00:00
Claude eba4cb0774 fix(hit-parade,stock): retire le filtre montant (théorie invalidée) + tri stock négatif
Analyse de cohérence de stock sur le cas 487673 site 579 : 24 réceptions,
31 ventes brutes, 7 retours clients → 24 ventes nettes, stock final 3.
Le « 38 » affiché = 31 + 7 : signature de l'ancienne formule SUM(ABS)
qui additionnait les retours. La formule nette SUM(-qtemvt) déjà en place
donne 24 — le filtre mntmvtttc <> 0 (hypothèse de lignes fantômes 0 €)
était inutile : aucune ligne de ce type dans les données réelles. Retiré
pour rester strictement aligné sur le calcul de la Grille.

Gestion de stock : ORDER BY cs.qte (colonne affichée) au lieu de
cs.stockdispo (colonne différente) dans pgGetStockNegatif.
2026-07-07 06:46:45 +00:00
Claude ea19c5771e fix(hit-parade): exclut les lignes de quantité sans montant (corrections)
Cas réel : codein 487673 site 579 affichait 38 vendus au lieu de 24,
avec un CA pourtant exact (215,76 €) — mvtart contient des lignes
genremvt=3 portant une quantité mais aucun montant TTC (corrections /
régularisations), que les endpoints officiels de l'API excluent.

Ajout du filtre mntmvtttc IS NOT NULL AND <> 0 dans l'agrégation :
une vente réelle a toujours un montant TTC non nul. Validé sur
PostgreSQL local en reproduisant le cas : sans filtre 38/215,76 €
(= symptôme), avec filtre 24/215,76 € (= réalité). Le CA et les
requêtes Analytics ne changent pas (ces lignes valent 0 €).
2026-07-06 21:15:44 +00:00
Claude 159508132c fix(hit-parade): agrégation isolée sur mvtart×articles — corrige les chiffres faux des 2 magasins
La version précédente agrégeait les ventes avec les jointures fournisseur
(LATERAL par ligne de mouvement + fouident) dans la même passe : si
fouident contient des codes dupliqués, chaque mouvement était compté
plusieurs fois → chiffres faux sur les deux magasins, et le LATERAL
s'exécutait par mouvement (très lent).

Restructuration, validée sur 12 000 mouvements réels de l'API chargés
dans un PostgreSQL 16 local :
- CTE ventes : mvtart JOIN articles uniquement (1:1), SUM(-qtemvt) /
  SUM(-mntmvtttc) / SUM(margemvt) par (codein, site) — le calcul
  canonique de la Grille (pgGetMensuelByFournisseur), signes vérifiés
  sur les mouvements réels (ventes négatives, genremvt=3).
- CTE attrs : libellé/fournisseur/nomenclature joints APRÈS agrégation,
  dédupliqués par DISTINCT ON — aucune jointure ne peut plus fausser
  les sommes (totaux invariants aux doublons artfou1/fouident injectés).
- Même principe pour pgGetCaByFournisseur, pgGetStockNegatif,
  pgGetSansVente6Mois : sous-requête artfou1 DISTINCT ON au lieu du
  LATERAL par ligne.
2026-07-06 20:49:12 +00:00
Claude b7de347a99 fix(grid,hit-parade): convention de signe nette + lint React Compiler
- Grille (fallback API mensuel) : Math.abs remplacé par la négation sur
  qte_vendue/ca_ht (négatifs côté API) — les retours clients restent
  déduits au lieu d'être inversés en positif, même convention que le SQL.
- Hit Parade (client) : ColHeader extrait hors du composant (erreur
  'Cannot create components during render'), useMemo déplacés avant
  exportToExcel pour préserver la mémoïsation React Compiler, variable
  idx inutilisée supprimée. ESLint 0 erreur, build OK.
2026-07-06 20:28:03 +00:00
Claude d733755659 fix(hit-parade): ventes nettes par magasin — corrige les chiffres faussés (579)
La requête Hit Parade divergeait du calcul canonique de la Grille :
- SUM(ABS(qtemvt)) additionnait les retours clients comme des ventes au
  lieu de les déduire → quantités et CA gonflés sur le magasin ayant des
  retours/avoirs sur la période (visible sur 579).
- ABS(SUM(mntmvtttc)) inversait le signe d'un CA net négatif.
- GROUP BY no_id + jointure artfou1 preference=1 non dédupliquée pouvait
  produire plusieurs lignes par (codein, site), écrasées par le pivot.

Corrections :
- Hit Parade : ventes nettes SUM(-qtemvt) / SUM(-mntmvtttc), agrégation
  par (codein, site), fournisseur préféré via LATERAL ... LIMIT 1,
  stock agrégé par codein ; pivot en accumulation (+=) par sécurité.
- Analytics (même classe de bug) : pgGetCaByFournisseur et
  pgGetCaByNomenclature passent en CA net ; jointure fournisseur
  dédupliquée (LATERAL LIMIT 1) pour éviter le double comptage.
- Gestion de stock : pgGetStockNegatif et pgGetSansVente6Mois dédupliquent
  le fournisseur préféré (LATERAL LIMIT 1) pour éviter les lignes en double.

Audit complet du mapping par magasin (292/579) côté client : aucun champ
inversé (hit-parade, analytics, dashboard, grid, heatmap). Build OK.
2026-07-06 15:55:43 +00:00
R0m1k3 32f9a27563 Fix Qlik network column sorting 2026-06-22 12:12:48 +00:00
R0m1k3 abac5c5ee5 perf(qlik): optimize network sync extraction 2026-06-22 09:58:49 +00:00
R0m1k3 1cb6c2496d fix(qlik): retry engine aborts after selections 2026-06-22 06:13:05 +00:00
R0m1k3 f005c08ad6 fix(qlik): recreate hypercube per batch 2026-06-22 05:44:37 +00:00
R0m1k3 850852912f fix(qlik): track async sync job status 2026-06-21 19:08:04 +00:00
R0m1k3 e7bcba169c fix(qlik): extract by month and code batches 2026-06-21 18:24:53 +00:00
R0m1k3 c4eeab11d2 fix(qlik): select all articles per month 2026-06-21 16:43:36 +00:00
R0m1k3 649a1df267 fix(qlik): aggregate multi-month extraction correctly 2026-06-21 14:32:51 +00:00
R0m1k3 6036ea5600 fix(qlik): avoid abort on batched network extraction 2026-06-21 12:08:13 +00:00
R0m1k3 6d27c7935d fix: align qlik grid extraction date range 2026-06-21 11:26:49 +00:00
MichaelandClaude Opus 4.8 de8a435c14 fix(qlik): batch SelectValues to avoid Engine abort + drop couv stock metric
Large supplier (4816 codes) triggered Qlik Engine "Request aborted"
(code 15): a single SelectValues over all codes forced the hypercube to
calculate too many rows at once. Now select in 1200-code batches and
accumulate, keeping each calc small. 1417 codes worked before, 4816 did
not - chunking scales to any supplier.

Also remove the "Couverture de stock" network column end-to-end (grid,
types, Qlik clients, cache, schema, db-init), shifting the marge measure
index from 6 to 5 and the hypercube width from 7 to 6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 10:55:03 +02:00
MichaelandClaude Opus 4.8 8cc95cb387 fix(grid): align Sync Qlik button with summary-bar actions
The button was a two-line flex-col (button + date below), making it
taller than the single-line Export button next to it, so its button sat
visibly higher in the items-center row. Lay it out inline instead: the
"MAJ Qlik" date sits to the left (hidden under lg, kept in the tooltip),
the button keeps the 36px btn-action height and now aligns with siblings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 09:41:28 +02:00
MichaelandClaude Opus 4.8 5f364f7d52 feat(grid): refine grid page chrome (refined dense-pro, theme kept)
Redesign the grid page shell while preserving the Slate/teal Apple theme:
- header reworked into a branded bar: teal accent marker + uppercase
  eyebrow + supplier name as the hero, with "Magasin" and "Références"
  metric pills
- loading/error state shown as a teal (or error-red) accent chip
- "Valider" save button restyled with the brand gradient + soft shadow
- grid container radius/shadow aligned to the apple-card system

No change to table cell logic or the network columns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 09:38:14 +02:00
MichaelandClaude Opus 4.8 524faf4a7a feat(qlik): drop network rupture% metric and the "Sans vente 6 mois" grid tab
Stock Rupture % N saturated at 100% for high-presence products (degenerate
at the article x network grain), so remove it end to end: config id,
NetworkMetric, hypercube (width 8->7), cache, schema, enrich and grid
column. db-init drops the column on existing DBs.

Also remove the "Sans vente 6 mois" tab from the grid page: the grid now
renders directly without the tab switcher; delete the NoSalesTab component.
(The separate Stock negatif page keeps its own sans-vente tabs.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 09:32:03 +02:00
MichaelandClaude Opus 4.8 af93e362e0 chore(qlik): read measure expressions via GetMeasure/GetProperties
The qData expr path returned empty. Fetch the real master-measure
definition through GetMeasure -> GetProperties (qMeasure.qDef.qDef) and
log rupture, couverture and CA/mag expressions to understand why
Stock Rupture % saturates at 100% for high-presence products.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 09:20:43 +02:00
MichaelandClaude Opus 4.8 ae1deedd19 chore(qlik): log measure expressions to diagnose rupture%=100%
3 of 4 new measures now resolve to engine qIds and return varied values
(CA/mag, couverture, marge%). Stock Rupture % N resolves but returns a
constant 1.0 (100%) per article — a measure-semantics issue. Log the
resolved rupture & couverture expressions to understand the definition,
and make the enrich sample pick a non-zero match (the previous sample hit
a no-sales code and looked all-zero).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 09:01:51 +02:00
MichaelandClaude Opus 4.8 da73693ea0 fix(qlik): resolve 4 new measures by title, not QRS GUID
The new measures returned 0 because their qLibraryId was set to the QRS
object GUID, but the engine references master measures by qInfo.qId
(short codes like JhqJ) — an unknown GUID is silently ignored and yields
null. Resolve CA par Magasin / Couverture / Marge % / Rupture % by title
via the in-page MeasureList (whitespace+case normalized), falling back to
the configured id, and log the resolved ids.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 08:50:43 +02:00