From eba4cb07742329c0ac415223dc6b4dd9cf9e507c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 06:46:45 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix(hit-parade,stock):=20retire=20le=20filt?= =?UTF-8?q?re=20montant=20(th=C3=A9orie=20invalid=C3=A9e)=20+=20tri=20stoc?= =?UTF-8?q?k=20n=C3=A9gatif?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/lib/pg-ff-client.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/lib/pg-ff-client.ts b/src/lib/pg-ff-client.ts index 530530d..ef69a85 100644 --- a/src/lib/pg-ff-client.ts +++ b/src/lib/pg-ff-client.ts @@ -770,10 +770,9 @@ export interface HitParadeRow { * mouvements et fausser les sommes, et le LATERAL fournisseur ne s'exécute que * par article vendu (et non par ligne de mouvement). * - * Filtre mntmvtttc <> 0 : mvtart contient des lignes genremvt=3 de quantité - * sans montant (corrections/régularisations, ex: codein 487673 site 579 — - * 38 affiché au lieu de 24 réels, CA exact). Une vente réelle a toujours un - * montant TTC non nul ; les endpoints officiels de l'API les excluent aussi. + * Cas vérifié (codein 487673, site 579, jan→juil 2026) : 31 ventes brutes, + * 7 retours clients → 24 net. L'ancien SUM(ABS(qtemvt)) affichait 38 (retours + * additionnés) ; SUM(-qtemvt) donne 24, identique à l'API mensuel officielle. */ export async function pgGetHitParade(dateDebut: string, dateFin: string): Promise { const result = await pgNoParallel(sql` @@ -789,8 +788,6 @@ export async function pgGetHitParade(dateDebut: string, dateFin: string): Promis WHERE m.datmvt BETWEEN ${dateDebut}::date AND ${dateFin}::date AND m.site IN ('292', '579') AND m.genremvt = 3 - AND m.mntmvtttc IS NOT NULL - AND m.mntmvtttc <> 0 AND a.codein IS NOT NULL GROUP BY TRIM(a.codein::text), m.site ), @@ -979,7 +976,7 @@ export async function pgGetStockNegatif(site?: string): Promise Date: Tue, 21 Jul 2026 15:00:40 +0000 Subject: [PATCH 2/3] =?UTF-8?q?fix(grille):=20la=20grille=20refl=C3=A8te?= =?UTF-8?q?=20l'=C3=A9tat=20serveur=20courant=20des=20gammes=20(INIT=20rec?= =?UTF-8?q?harg=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é. --- src/features/grid/api/get-product-rows.ts | 29 +++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/features/grid/api/get-product-rows.ts b/src/features/grid/api/get-product-rows.ts index 725257f..1ae30c2 100644 --- a/src/features/grid/api/get-product-rows.ts +++ b/src/features/grid/api/get-product-rows.ts @@ -313,7 +313,15 @@ async function buildProductRows(input: GetProductRowsInput): Promise 0) { const changes = snaps[0].changes as Record; + let enAttente = 0; + let obsoletes = 0; for (const [codein, change] of Object.entries(changes)) { const product = productMap.get(codein); - if (product && change.after) { - // codeGammeInit reste figé — seulement codeGamme est overridé + if (!product || !change.after) continue; + + const gammeLive = product.codeGammeInit; // état serveur courant + const before = change.before ?? null; + const stillPending = before === (gammeLive ?? null) && change.after !== gammeLive; + + if (stillPending) { + // Modification pas encore appliquée sur le serveur → on la remonte product.codeGamme = change.after as GammeCode; + enAttente++; + } else { + // Déjà appliquée côté serveur (ou serveur divergent) → on garde le LIVE + product.codeGamme = gammeLive; + obsoletes++; } } - console.log(`[getProductRows] Gammes restaurées depuis snapshot`); + console.log(`[getProductRows] Snapshot: ${enAttente} modif(s) en attente réappliquée(s), ${obsoletes} déjà appliquée(s)/obsolète(s) ignorée(s)`); } } catch (snapErr) { console.error("[getProductRows] Snapshot restore error:", snapErr); From f0fdab465c9f555c5c648b2c30eedc5ce34a703f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 15:05:02 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix(grille):=20colonne=20INIT=20recharg?= =?UTF-8?q?=C3=A9e=20=C3=A0=20chaque=20chargement=20(m=C3=AAme=20sur=20cac?= =?UTF-8?q?he)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/features/grid/api/get-product-rows.ts | 55 ++++++++++++----------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/features/grid/api/get-product-rows.ts b/src/features/grid/api/get-product-rows.ts index 1ae30c2..376232c 100644 --- a/src/features/grid/api/get-product-rows.ts +++ b/src/features/grid/api/get-product-rows.ts @@ -47,10 +47,33 @@ export function invalidateGridRowsCache(codeFournisseur?: string) { gridRowsPending.clear(); } +/** + * Rafraîchit UNIQUEMENT la colonne INIT (codeGammeInit = gamme serveur courante) + * sur des lignes servies depuis le cache. La colonne Gamme (codeGamme) n'est pas + * touchée : elle conserve la valeur issue du snapshot. Ainsi l'état serveur (INIT) + * est toujours à jour à chaque chargement, même sur un hit du cache 10 min — et le + * marqueur "modifié" (Gamme ≠ INIT) se résorbe dès que le serveur rattrape la modif. + */ +async function refreshGammeInit(rows: ProductRow[], codeFournisseur: string): Promise { + try { + const gammeMap = await pgGetGammesByFournisseur(codeFournisseur); + if (gammeMap.size === 0) return; + for (const row of rows) { + const live = gammeMap.get(row.codein); + if (live !== undefined) row.codeGammeInit = live as GammeCode; + } + } catch (e) { + console.error("[getProductRows] refreshGammeInit error:", (e as Error).message?.slice(0, 200)); + } +} + export async function getProductRows(input: GetProductRowsInput): Promise { const cacheKey = gridRowsCacheKey(input); const cached = gridRowsCache.get(cacheKey); if (!input.forceRefresh && cached && Date.now() - cached.createdAt < GRID_ROWS_CACHE_TTL_MS) { + // La colonne INIT doit refléter l'état serveur à CHAQUE chargement, + // y compris sur un hit de cache (les données lourdes restent, elles, cachées). + await refreshGammeInit(cached.rows, input.codeFournisseur); return cached.rows; } @@ -313,15 +336,10 @@ async function buildProductRows(input: GetProductRowsInput): Promise 0) { const changes = snaps[0].changes as Record; - let enAttente = 0; - let obsoletes = 0; for (const [codein, change] of Object.entries(changes)) { const product = productMap.get(codein); - if (!product || !change.after) continue; - - const gammeLive = product.codeGammeInit; // état serveur courant - const before = change.before ?? null; - const stillPending = before === (gammeLive ?? null) && change.after !== gammeLive; - - if (stillPending) { - // Modification pas encore appliquée sur le serveur → on la remonte + if (product && change.after) { + // codeGammeInit reste figé (état serveur) — seul codeGamme est overridé product.codeGamme = change.after as GammeCode; - enAttente++; - } else { - // Déjà appliquée côté serveur (ou serveur divergent) → on garde le LIVE - product.codeGamme = gammeLive; - obsoletes++; } } - console.log(`[getProductRows] Snapshot: ${enAttente} modif(s) en attente réappliquée(s), ${obsoletes} déjà appliquée(s)/obsolète(s) ignorée(s)`); + console.log(`[getProductRows] Gammes restaurées depuis snapshot`); } } catch (snapErr) { console.error("[getProductRows] Snapshot restore error:", snapErr);