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.
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.
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é.
- 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.
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>
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>
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>
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>
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>
Adds a sample log of the first extracted rows (all 8 hypercube columns)
and a sample of the network metrics matched from cache in getProductRows,
to pinpoint whether the new measures return null from Qlik, fail to
persist, or fail to render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds CA par Magasin, Couverture stock Qte, Marge % and Stock Rupture %
network measures to the Qlik hypercube extraction (in-page Playwright ws
+ legacy server ws), persists them in qlik_network_metrics, joins them in
getProductRows and renders 4 new grid columns with buy-signal coloring
(low coverage / high margin = green; high rupture = rose).
- qlik-client: config IDs (env-overridable, master GUIDs of app 9872ee6e),
NetworkMetric fields, hypercube width 4->8, page height 2500->1250
- qlik-playwright: same 4 measures in the in-page hypercube + row mapping
- schema + db-init: 4 numeric columns (ALTER ADD COLUMN IF NOT EXISTS for
existing DBs)
- cache + grid types + enrich: plumb the new fields end to end
Note: percent measures stored as raw Qlik ratio; UI renders x100 when |v|<=1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restore the _refresh searchParam bump so grid-client refetches
/api/grid/rows with refresh=1 (forceRefresh, bypassing the 10-min
getProductRows cache) and re-renders the network columns from the
store. Also call router.refresh() to update the server-rendered
MAJ Qlik timestamp on the button.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a second tab "Sans vente 6 mois" on the grid page listing all
products with no sale in the last 6 months, sorted by oldest last
sale first. Resolves merge conflicts from upstream refactor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of unreliable preference=1, use artfou1.no_id DESC (most recently
added artfou1 entry) to identify the last supplier for each product.
Shows an amber warning icon in the Désignation column when the last
supplier ≠ current supplier view, with tooltip showing who to attribute
the sales to.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- FloatingSummaryBar: hide BulkAiAnalyzer button for non-admins
- AiInsightBlock: hide individual Analyser button for non-admins
- Both use useSession() to check role === "admin"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BulkAiAnalyzer: set isAnalyzing=true immediately on click (before
context fetch), so the spinner appears instantly instead of after
1-2 seconds of silence
- analyze/route.ts: if configured model returns 400/404 (e.g., deprecated
google/gemini-flash-1.5), automatically retry with google/gemini-2.0-flash-001
- batch-analyze/route.ts: same fallback logic
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove infinite grid loading, useInfiniteGrid hook, and server-side
pagination API route. Revert grid components to the simpler version
that avoids 2MB+ cache failures from unstable_cache.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Non-admin users see gamme as read-only badge (no select)
- Export button hidden for non-admin in grid
- Save drafts button hidden for non-admin
- Exports page hidden in sidebar for non-admin
- Create user button: explicit blue style (was unreadable with apple-btn-primary)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The ranking bulk endpoint is capped at 500 by the API server, making
it useless for full supplier matching. Per-article ranking (codein filter)
works correctly and is now called only for articles WITH sales data.
getProductRows restructured:
- Phase 2A: mensuel + commandes in parallel (all articles)
- Phase 2B: referentiel + ranking in parallel, only for articlesWithData
(articles that appear in mensuelMap)
For a 45060-article supplier with 5446 with sales:
- Referentiel: 45060 → 5446 calls (88% reduction)
- Ranking: 45060 → 5446 calls (88% reduction)
totalRankedProducts fetched from GET /api/ranking?limit=500 count.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove actif/suspendu filter that caused wrong stats (893€ CA, 249 units)
by incorrectly excluding products with sales. The parallel pagination
and bulk ranking fixes are sufficient to handle large suppliers.
- Increase getMensuelByArticles and getReferentielByArticles batchSize
from 20 to 50, reducing sequential batch iterations by ~2.5x.
- Filter gamme Y products with zero 12-month sales from returned rows.
Reduces grid size (e.g. S053: 14775→fewer rows) without affecting
products that actually sold. Y products with sales are preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>