- 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.
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.
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').
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>
Add a "Qlik Sense — Données réseau" section in Paramètres (host/user/password
+ Save & Test buttons), stored in data/.db-config.json. qlik-client reads this
file first (env fallback), so no env vars needed for the sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le build Docker échouait sur le fetch de la police Inter via next/font/google
(accès réseau/TLS indisponible pendant 'next build').
- layout.tsx : charge Inter via <link> côté navigateur (plus de fetch au build),
avec fallback système ; --font-sans défini dans globals.css (@theme).
- /commandes-auto : export dynamic = 'force-dynamic' (données live DB + API,
jamais prérendues au build).
- listCadences : résilient si la DB est injoignable (retourne []).
Ajoute un onglet "Cadencier / Alertes" dans la page Commandes auto.
Depuis la liste des fournisseurs, on active un fournisseur en alerte de
commande sur un magasin (292/579) avec une fréquence en X semaines,
gérée indépendamment par magasin.
L'échéance est calculée à partir de la dernière réception réelle
(mvtart, genremvt 1/2, fournisseur principal artfou1.preference=1) +
X semaines. Statut: à commander / bientôt / OK, avec temps restant.
- Table commande_cadences (schema + db-init)
- pgGetDerniereReceptionParFournisseur (mvtart par fournisseur/site)
- Server actions CRUD (list/upsert/toggle/remove)
- UI à onglets + cadencier (ajout, édition intervalle, activer/supprimer)
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>
- Replace month dropdown with date picker (Du/Au) + Appliquer button
- Add fournisseur filter dropdown (client-side, all suppliers from current data)
- Totals update live when fournisseur filter changes
- SQL: use BETWEEN date range instead of TO_CHAR month match
- Create user button: proper 3-col form layout so button stays on one line
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>