From 524faf4a7a4190fbf21d41af2b2b473224b482f7 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 21 Jun 2026 09:32:03 +0200 Subject: [PATCH] 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 --- scripts/db-init.js | 5 +- src/db/schema.ts | 2 - src/features/grid/api/get-product-rows.ts | 1 - src/features/grid/components/grid-client.tsx | 78 ++------ src/features/grid/components/heatmap-grid.tsx | 21 +-- src/features/grid/components/no-sales-tab.tsx | 178 ------------------ src/lib/qlik-client.ts | 12 +- src/lib/qlik-network-cache.ts | 4 - src/lib/qlik-playwright.ts | 32 +--- src/types/grid.ts | 2 - 10 files changed, 32 insertions(+), 303 deletions(-) delete mode 100644 src/features/grid/components/no-sales-tab.tsx diff --git a/scripts/db-init.js b/scripts/db-init.js index 1da21c1..8b97fba 100644 --- a/scripts/db-init.js +++ b/scripts/db-init.js @@ -105,7 +105,6 @@ async function main() { "ca_par_magasin_reseau" numeric(16, 2), "couverture_stock_reseau" numeric(12, 3), "marge_pct_reseau" numeric(8, 4), - "rupture_pct_reseau" numeric(8, 4), "periode" varchar(20), "fetched_at" timestamp DEFAULT now() ); @@ -115,8 +114,8 @@ async function main() { ALTER TABLE "qlik_network_metrics" ADD COLUMN IF NOT EXISTS "ca_par_magasin_reseau" numeric(16, 2), ADD COLUMN IF NOT EXISTS "couverture_stock_reseau" numeric(12, 3), - ADD COLUMN IF NOT EXISTS "marge_pct_reseau" numeric(8, 4), - ADD COLUMN IF NOT EXISTS "rupture_pct_reseau" numeric(8, 4); + ADD COLUMN IF NOT EXISTS "marge_pct_reseau" numeric(8, 4); + ALTER TABLE "qlik_network_metrics" DROP COLUMN IF EXISTS "rupture_pct_reseau"; `); console.log("[DB Init] Table qlik_network_metrics is verified/created."); diff --git a/src/db/schema.ts b/src/db/schema.ts index f67366d..b16f1b0 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -123,8 +123,6 @@ export const qlikNetworkMetrics = pgTable("qlik_network_metrics", { couvertureStockReseau: numeric("couverture_stock_reseau", { precision: 12, scale: 3 }), /** Taux de marge réseau (ratio brut Qlik) */ margePctReseau: numeric("marge_pct_reseau", { precision: 8, scale: 4 }), - /** Taux de rupture réseau (ratio brut Qlik) */ - rupturePctReseau: numeric("rupture_pct_reseau", { precision: 8, scale: 4 }), /** Période couverte (libre, ex "12m" ou "2025") */ periode: varchar("periode", { length: 20 }), /** Dernière synchro depuis Qlik */ diff --git a/src/features/grid/api/get-product-rows.ts b/src/features/grid/api/get-product-rows.ts index 568cb6d..0d475df 100644 --- a/src/features/grid/api/get-product-rows.ts +++ b/src/features/grid/api/get-product-rows.ts @@ -486,7 +486,6 @@ async function enrichWithNetworkMetrics(productMap: Map): Pr product.caParMagasinReseau = m.caParMagasinReseau; product.couvertureStockReseau = m.couvertureStockReseau; product.margePctReseau = m.margePctReseau; - product.rupturePctReseau = m.rupturePctReseau; product.tauxPresenceReseau = m.nbMagasinsReseau / NB_MAGASINS_RESEAU; product.networkFetchedAt = m.fetchedAt ?? undefined; } diff --git a/src/features/grid/components/grid-client.tsx b/src/features/grid/components/grid-client.tsx index 253b889..a8829d7 100644 --- a/src/features/grid/components/grid-client.tsx +++ b/src/features/grid/components/grid-client.tsx @@ -6,7 +6,6 @@ import { FloatingSummaryBar } from "@/features/grid/components/floating-summary- import { BulkActionToolbar } from "@/features/grid/components/bulk-action-toolbar"; import { GridFilterBar } from "@/features/grid/components/grid-filter-bar"; import { ExportDropdown } from "@/features/grid/components/export-dropdown"; -import { NoSalesTab, countNoSalesIn6Months } from "@/features/grid/components/no-sales-tab"; import { useGridStore } from "@/features/grid/store/use-grid-store"; import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts"; import type { ProductRow } from "@/types/grid"; @@ -44,15 +43,12 @@ export function GridClient({ codeFournisseur, nomFournisseur, fournisseurs, maga const [selectedCodeins, setSelectedCodeins] = useState([]); const [isPending, startTransition] = useTransition(); const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle"); - const [activeTab, setActiveTab] = useState<"grid" | "no-sales">("grid"); const [isLoadingRows, setIsLoadingRows] = useState(false); const [rowsLoaded, setRowsLoaded] = useState(0); const [totalRows, setTotalRows] = useState(null); const [loadError, setLoadError] = useState(null); const [showStartOverlay, setShowStartOverlay] = useState(false); - const noSalesCount = useMemo(() => countNoSalesIn6Months(rows), [rows]); - const visibleCodeins = useMemo(() => rows.map(r => r.codein), [rows]); const { save, hasDrafts, count } = useSaveDrafts(magasin, visibleCodeins); @@ -234,68 +230,26 @@ export function GridClient({ codeFournisseur, nomFournisseur, fournisseurs, maga - {/* Tab switcher */} -
- {(["grid", "no-sales"] as const).map(tab => { - const isActive = activeTab === tab; - return ( - - ); - })} + {/* Bulk toolbar (contextual) */} +
+ setSelectedCodeins([])} + />
- {/* Bulk toolbar (contextual) — uniquement sur l'onglet grille */} - {activeTab === "grid" && ( -
- setSelectedCodeins([])} - /> -
- )} - {/* Main content */} - {activeTab === "grid" ? ( -
- -
- ) : ( -
- -
- )} +
+ +
- {/* Summary bar — uniquement sur l'onglet grille */} - {activeTab === "grid" && ( -
- -
- )} + {/* Summary bar */} +
+ +
{/* Overlay de démarrage premium (Glassmorphism semi-bloquant) */} {showStartOverlay && isLoadingRows && ( diff --git a/src/features/grid/components/heatmap-grid.tsx b/src/features/grid/components/heatmap-grid.tsx index bb67f69..7e69360 100644 --- a/src/features/grid/components/heatmap-grid.tsx +++ b/src/features/grid/components/heatmap-grid.tsx @@ -178,7 +178,7 @@ const GridRow = React.memo(({ virtualRow, row, rowHeight, isSelected, columnVisi > {row.getVisibleCells().map((cell: Cell) => { const isFlexible = cell.column.id === "libelle1" || cell.column.id === "libelle3"; - const isCenter = cell.column.id === "totalQuantite" || cell.column.id === "totalCa" || cell.column.id === "totalMarge" || cell.column.id.startsWith("month_") || cell.column.id === "gammeInitial" || cell.column.id === "caReseau" || cell.column.id === "qteReseau" || cell.column.id === "nbMagasinsReseau" || cell.column.id === "caParMagasinReseau" || cell.column.id === "couvertureStockReseau" || cell.column.id === "margePctReseau" || cell.column.id === "rupturePctReseau" || cell.column.id === "tauxPresenceReseau" || cell.column.id === "gamme"; + const isCenter = cell.column.id === "totalQuantite" || cell.column.id === "totalCa" || cell.column.id === "totalMarge" || cell.column.id.startsWith("month_") || cell.column.id === "gammeInitial" || cell.column.id === "caReseau" || cell.column.id === "qteReseau" || cell.column.id === "nbMagasinsReseau" || cell.column.id === "caParMagasinReseau" || cell.column.id === "couvertureStockReseau" || cell.column.id === "margePctReseau" || cell.column.id === "tauxPresenceReseau" || cell.column.id === "gamme"; const size = cell.column.getSize(); return (
Rupt. %
Réseau
, - size: 75, - cell: ({ getValue }) => { - const val = getValue(); - if (val == null) return
-
; - const pct = Math.abs(val) <= 1 ? val * 100 : val; - // Rupture haute = demande non servie = signal, mais aussi tension stock - const color = pct >= 15 ? "text-rose-500" : pct >= 5 ? "text-amber-500" : "text-emerald-500"; - return ( -
- {pct.toLocaleString("fr-FR", { maximumFractionDigits: 1 })}% -
- ); - }, - }, ...MONTHS_12.map((monthKey) => ({ id: `month_${monthKey}`, header: () =>
{formatMonthLabel(monthKey)}
, @@ -840,7 +823,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) { {headerGroup.headers.map((header) => { const isFlexible = header.column.id === "libelle1" || header.column.id === "libelle3"; - const isCenter = header.column.id === "totalQuantite" || header.column.id === "totalCa" || header.column.id === "totalMarge" || header.column.id.startsWith("month_") || header.column.id === "gammeInitial" || header.column.id === "caReseau" || header.column.id === "qteReseau" || header.column.id === "nbMagasinsReseau" || header.column.id === "caParMagasinReseau" || header.column.id === "couvertureStockReseau" || header.column.id === "margePctReseau" || header.column.id === "rupturePctReseau" || header.column.id === "tauxPresenceReseau" || header.column.id === "gamme"; + const isCenter = header.column.id === "totalQuantite" || header.column.id === "totalCa" || header.column.id === "totalMarge" || header.column.id.startsWith("month_") || header.column.id === "gammeInitial" || header.column.id === "caReseau" || header.column.id === "qteReseau" || header.column.id === "nbMagasinsReseau" || header.column.id === "caParMagasinReseau" || header.column.id === "couvertureStockReseau" || header.column.id === "margePctReseau" || header.column.id === "tauxPresenceReseau" || header.column.id === "gamme"; const size = header.getSize(); return ( { - if (row.derniereVente) { - return new Date(row.derniereVente) < sixMonthsAgo; - } - // Fallback : calcul depuis sales12m - const salesMonths = Object.entries(row.sales12m || {}) - .filter(([, qty]) => qty > 0) - .map(([m]) => m) - .sort(); - if (salesMonths.length === 0) return true; - const last = salesMonths[salesMonths.length - 1]; - const lastDate = new Date(parseInt(last.slice(0, 4)), parseInt(last.slice(4, 6)) - 1, 1); - return lastDate < sixMonthsAgo; - }); -} - -function formatDate(iso?: string): string { - if (!iso) return "—"; - return new Date(iso).toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric" }); -} - -function daysSince(iso?: string): number | null { - if (!iso) return null; - return Math.floor((Date.now() - new Date(iso).getTime()) / 86_400_000); -} - -function formatCa(ca: number): string { - return ca.toLocaleString("fr-FR", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }); -} - -const GAMME_COLORS: Record = { - A: "bg-emerald-500/15 text-emerald-700", - B: "bg-blue-500/15 text-blue-700", - C: "bg-amber-500/15 text-amber-700", - Y: "bg-purple-500/15 text-purple-700", - Z: "bg-red-500/15 text-red-700", -}; - -export function NoSalesTab() { - const rows = useGridStore(s => s.rows); - const scrollRef = useRef(null); - - const filtered = useMemo(() => { - const result = getNoSalesIn6Months(rows); - // Tri : produits les plus anciens en premier, puis par stock décroissant - return result.sort((a, b) => { - const da = a.derniereVente ? new Date(a.derniereVente).getTime() : 0; - const db = b.derniereVente ? new Date(b.derniereVente).getTime() : 0; - if (da !== db) return da - db; - return (b.stockActuel ?? 0) - (a.stockActuel ?? 0); - }); - }, [rows]); - - // eslint-disable-next-line react-hooks/incompatible-library - const rowVirtualizer = useVirtualizer({ - count: filtered.length, - getScrollElement: () => scrollRef.current, - estimateSize: () => 36, - overscan: 12, - }); - - if (rows.length === 0) { - return ( -
- Chargement des données en cours… -
- ); - } - - if (filtered.length === 0) { - return ( -
- -

Aucun produit sans vente depuis 6 mois.

-
- ); - } - - return ( -
- {/* Résumé */} -
- - - - {filtered.length.toLocaleString("fr-FR")} - - {" "}produit{filtered.length > 1 ? "s" : ""} sans vente depuis plus de 6 mois - {" "}({rows.length.toLocaleString("fr-FR")} références au total) - -
- - {/* Tableau */} -
-
-
- {["Code", "Libellé", "Réf.", "Gamme", "Dernière vente", "Jours", "Stock", "CA 12m", "Mag."].map(col => ( -
- {col} -
- ))} -
-
- {rowVirtualizer.getVirtualItems().map((virtualRow) => { - const row = filtered[virtualRow.index]; - const i = virtualRow.index; - const days = daysSince(row.derniereVente); - const gamme = row.codeGamme?.trim().toUpperCase() ?? "—"; - const gammeClass = GAMME_COLORS[gamme] ?? "bg-zinc-500/15 text-zinc-600"; - return ( -
-
{row.codein}
-
{row.libelle1}
-
{row.reference || "—"}
-
- - {gamme} - -
-
- {formatDate(row.derniereVente)} -
-
365 ? "var(--color-red, #ef4444)" : "var(--text-secondary)" }}> - {days != null ? days.toLocaleString("fr-FR") : "—"} -
-
- {row.stockActuel != null ? row.stockActuel.toLocaleString("fr-FR") : "—"} -
-
- {row.totalCa > 0 ? formatCa(row.totalCa) : "—"} -
-
- {row.nbMagasinsReseau != null ? row.nbMagasinsReseau : "—"} -
-
- ); - })} -
-
-
-
- ); -} - -/** Compte les produits sans vente depuis 6 mois — utilisé pour le badge dans le switcher */ -export function countNoSalesIn6Months(rows: ProductRow[]): number { - return getNoSalesIn6Months(rows).length; -} diff --git a/src/lib/qlik-client.ts b/src/lib/qlik-client.ts index abcec86..f8e8cf0 100644 --- a/src/lib/qlik-client.ts +++ b/src/lib/qlik-client.ts @@ -51,7 +51,6 @@ export interface QlikConfig { measCaMagId: string; // master measure "CA par Magasin N" measCouvQteId: string; // master measure "Couverture de stock Quantite N" measMargePctId: string; // master measure "Marge % N" - measRuptPctId: string; // master measure "Stock Rupture % N" tlsInsecure: boolean; timeoutMs: number; } @@ -76,7 +75,6 @@ export function getQlikConfig(): QlikConfig { measCaMagId: process.env.QLIK_MEAS_CAMAG_ID ?? "16778c00-a4e0-41c7-ab1c-179810d78172", measCouvQteId: process.env.QLIK_MEAS_COUVQTE_ID ?? "dbdf957d-a380-495f-bdc4-3f7b6688f3b5", measMargePctId: process.env.QLIK_MEAS_MARGEPCT_ID ?? "ea1e8feb-49ba-4eb3-8db6-72dcfb4c1222", - measRuptPctId: process.env.QLIK_MEAS_RUPTPCT_ID ?? "a60a5c1a-5966-484c-9771-dbe55bd83a3c", tlsInsecure: (process.env.QLIK_TLS_INSECURE ?? "true") === "true", timeoutMs: Number(process.env.QLIK_TIMEOUT_MS ?? "60000"), }; @@ -93,8 +91,6 @@ export interface NetworkMetric { couvertureStockReseau: number; /** Taux de marge réseau (ratio brut Qlik, ex 0.32 = 32%) */ margePctReseau: number; - /** Taux de rupture réseau (ratio brut Qlik, ex 0.05 = 5%) */ - rupturePctReseau: number; periode?: string; } @@ -250,9 +246,8 @@ function hyperCubeDef(cfg: QlikConfig, height: number) { { qLibraryId: cfg.measCaMagId }, { qLibraryId: cfg.measCouvQteId }, { qLibraryId: cfg.measMargePctId }, - { qLibraryId: cfg.measRuptPctId }, ], - qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 8, qHeight: height }], + qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 7, qHeight: height }], qSuppressZero: false, qSuppressMissing: true, }, @@ -275,7 +270,7 @@ export async function fetchNetworkMetrics( const openRes = await conn.rpc("OpenDoc", { qDocName: cfg.appNetwork }); const docHandle = (((openRes.qReturn as Record) ?? {}).qHandle as number) ?? 1; - const pageHeight = 1250; // 8 colonnes * 1250 = 10000 cellules max + const pageHeight = 1400; // 7 colonnes * 1400 = 9800 cellules max const createRes = await conn.rpc("CreateSessionObject", { qProp: hyperCubeDef(cfg, pageHeight) }, docHandle); const objHandle = (((createRes.qReturn as Record) ?? {}).qHandle as number); @@ -283,7 +278,7 @@ export async function fetchNetworkMetrics( for (;;) { const dataRes = await conn.rpc( "GetHyperCubeData", - { qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: 8, qHeight: pageHeight }] }, + { qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: 7, qHeight: pageHeight }] }, objHandle, ); const pages = (dataRes.qDataPages as Array<{ qMatrix?: Matrix }>) ?? []; @@ -302,7 +297,6 @@ export async function fetchNetworkMetrics( caParMagasinReseau: Number(row[4]?.qNum ?? 0) || 0, couvertureStockReseau: Number(row[5]?.qNum ?? 0) || 0, margePctReseau: Number(row[6]?.qNum ?? 0) || 0, - rupturePctReseau: Number(row[7]?.qNum ?? 0) || 0, }); } if (matrix.length < pageHeight) break; diff --git a/src/lib/qlik-network-cache.ts b/src/lib/qlik-network-cache.ts index ea65f7b..a25c662 100644 --- a/src/lib/qlik-network-cache.ts +++ b/src/lib/qlik-network-cache.ts @@ -18,7 +18,6 @@ export interface NetworkMetricCached { caParMagasinReseau: number; couvertureStockReseau: number; margePctReseau: number; - rupturePctReseau: number; fetchedAt: string | null; } @@ -43,7 +42,6 @@ export async function getNetworkMetricsByCodeCentrale( caParMagasinReseau: Number(r.caParMagasinReseau ?? 0) || 0, couvertureStockReseau: Number(r.couvertureStockReseau ?? 0) || 0, margePctReseau: Number(r.margePctReseau ?? 0) || 0, - rupturePctReseau: Number(r.rupturePctReseau ?? 0) || 0, fetchedAt: r.fetchedAt ? new Date(r.fetchedAt).toISOString() : null, }); } @@ -62,7 +60,6 @@ export async function upsertNetworkMetrics(metrics: NetworkMetric[]): Promise + ({ app, dim, mca, mqte, mnb, mcamag, mcouv, mmarge, codes, token }: { app: string; dim: string; mca: string; mqte: string; mnb: string; mcamag: string; mcouv: string; mmarge: string; codes: string[]; token: string }) => new Promise((resolve) => { const loc = (window as unknown as { location: Location }).location; const url = `wss://${loc.host}/app/${app}?reloadUri=${encodeURIComponent(loc.href)}&qlik-csrf-token=${token}`; @@ -121,24 +121,10 @@ export async function fetchNetworkMetricsPlaywright( const rCamag = pickMeasure("CA par Magasin N", mcamag); const rCouv = pickMeasure("Couverture de stock Quantité N", mcouv); const rMarge = pickMeasure("Marge % N", mmarge); - const rRupt = pickMeasure("Stock Rupture % N", mrupt); - console.log("measures résolues par titre: " + JSON.stringify({ rCamag, rCouv, rMarge, rRupt }) + " (sur " + items.length + " mesures)"); - // Lit l'expression réelle d'une master measure (GetMeasure → GetProperties). - const measExpr = async (qId: string): Promise => { - try { - const gm = await rpc("GetMeasure", { qId }, doc); - const h = (gm.qReturn as { qHandle: number }).qHandle; - const p = await rpc("GetProperties", {}, h); - const def = (p.qProp as { qMeasure?: { qDef?: { qDef?: string; qLabel?: string } } })?.qMeasure?.qDef; - return def?.qDef ?? def?.qLabel ?? JSON.stringify(def ?? "n/a"); - } catch (e) { return "err:" + String((e as Error)?.message || e); } - }; - console.log("expr rupture (" + rRupt + "): " + await measExpr(rRupt)); - console.log("expr couverture (" + rCouv + "): " + await measExpr(rCouv)); - console.log("expr camag (" + rCamag + "): " + await measExpr(rCamag)); + console.log("measures résolues par titre: " + JSON.stringify({ rCamag, rCouv, rMarge }) + " (sur " + items.length + " mesures)"); const obj = await rpc("CreateSessionObject", { qProp: { qInfo: { qType: "cf-net" }, qHyperCubeDef: { qDimensions: [{ qLibraryId: dim }], - qMeasures: [{ qLibraryId: mca }, { qLibraryId: mqte }, { qLibraryId: mnb }, { qLibraryId: rCamag }, { qLibraryId: rCouv }, { qLibraryId: rMarge }, { qLibraryId: rRupt }], + qMeasures: [{ qLibraryId: mca }, { qLibraryId: mqte }, { qLibraryId: mnb }, { qLibraryId: rCamag }, { qLibraryId: rCouv }, { qLibraryId: rMarge }], qInitialDataFetch: [], } } }, doc); const oh = (obj.qReturn as { qHandle: number }).qHandle; @@ -146,12 +132,12 @@ export async function fetchNetworkMetricsPlaywright( const size = ((layout.qLayout as { qHyperCube: { qSize: { qcy: number } } }).qHyperCube).qSize.qcy; console.log("cube qSize.qcy=" + size + " lignes"); const out: Array> = []; - const PAGE = 1250; + const PAGE = 1400; for (let top = 0; top < size; top += PAGE) { - const d = await rpc("GetHyperCubeData", { qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: 8, qHeight: PAGE }] }, oh); + const d = await rpc("GetHyperCubeData", { qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: 7, qHeight: PAGE }] }, oh); const matrix = (d.qDataPages as Array<{ qMatrix?: Array> }>)?.[0]?.qMatrix ?? []; if (!matrix.length) break; - for (const r of matrix) out.push([String(r[0]?.qText ?? ""), Number(r[1]?.qNum) || 0, Number(r[2]?.qNum) || 0, Number(r[3]?.qNum) || 0, Number(r[4]?.qNum) || 0, Number(r[5]?.qNum) || 0, Number(r[6]?.qNum) || 0, Number(r[7]?.qNum) || 0]); + for (const r of matrix) out.push([String(r[0]?.qText ?? ""), Number(r[1]?.qNum) || 0, Number(r[2]?.qNum) || 0, Number(r[3]?.qNum) || 0, Number(r[4]?.qNum) || 0, Number(r[5]?.qNum) || 0, Number(r[6]?.qNum) || 0]); if (matrix.length < PAGE) break; } ws.close(); @@ -159,7 +145,7 @@ export async function fetchNetworkMetricsPlaywright( } catch (e) { try { ws.close(); } catch { /* noop */ } fail(String((e as Error)?.message || e)); } })(); }), - { app: cfg.appNetwork, dim: cfg.dimCodeArticleId, mca: cfg.measCaId, mqte: cfg.measQteId, mnb: cfg.measNbMagId, mcamag: cfg.measCaMagId, mcouv: cfg.measCouvQteId, mmarge: cfg.measMargePctId, mrupt: cfg.measRuptPctId, codes: codeCentraux ?? [], token: csrfToken }, + { app: cfg.appNetwork, dim: cfg.dimCodeArticleId, mca: cfg.measCaId, mqte: cfg.measQteId, mnb: cfg.measNbMagId, mcamag: cfg.measCaMagId, mcouv: cfg.measCouvQteId, mmarge: cfg.measMargePctId, codes: codeCentraux ?? [], token: csrfToken }, )) as InPageResult; if (!result.ok) throw new Error(`[qlik-pw] ${result.error}`); @@ -170,12 +156,12 @@ export async function fetchNetworkMetricsPlaywright( const code = String(r[0]).trim(); if (!code || code === "-") continue; if (wanted && !wanted.has(code)) continue; - out.set(code, { codeCentrale: code, caReseau: Number(r[1]) || 0, qteReseau: Number(r[2]) || 0, nbMagasinsReseau: Number(r[3]) || 0, caParMagasinReseau: Number(r[4]) || 0, couvertureStockReseau: Number(r[5]) || 0, margePctReseau: Number(r[6]) || 0, rupturePctReseau: Number(r[7]) || 0 }); + out.set(code, { codeCentrale: code, caReseau: Number(r[1]) || 0, qteReseau: Number(r[2]) || 0, nbMagasinsReseau: Number(r[3]) || 0, caParMagasinReseau: Number(r[4]) || 0, couvertureStockReseau: Number(r[5]) || 0, margePctReseau: Number(r[6]) || 0 }); } console.log(`[qlik-pw] ${out.size} produits réseau (cube ${result.size})`); // Échantillon brut pour vérifier que les 4 nouvelles mesures (cols 4-7) renvoient des valeurs const sample = (result.rows ?? []).slice(0, 3).map((r) => ({ - code: r[0], ca: r[1], qte: r[2], nbMag: r[3], caMag: r[4], couv: r[5], margePct: r[6], ruptPct: r[7], + code: r[0], ca: r[1], qte: r[2], nbMag: r[3], caMag: r[4], couv: r[5], margePct: r[6], })); console.log(`[qlik-pw] échantillon lignes:`, JSON.stringify(sample)); return out; diff --git a/src/types/grid.ts b/src/types/grid.ts index 7f8e88e..1bc32a9 100644 --- a/src/types/grid.ts +++ b/src/types/grid.ts @@ -119,8 +119,6 @@ export interface ProductRow { couvertureStockReseau?: number; /** Taux de marge réseau (ratio brut Qlik, ex 0.32 = 32%) */ margePctReseau?: number; - /** Taux de rupture réseau (ratio brut Qlik, ex 0.05 = 5%) */ - rupturePctReseau?: number; /** Taux de présence réseau = nbMagasinsReseau / 270 */ tauxPresenceReseau?: number; /** Fraîcheur des données réseau (ISO) */