feat(qlik): extract 4 decision metrics (CA/mag, couverture, marge%, rupture%)

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>
This commit is contained in:
MichaelandClaude Opus 4.8 committed 2026-06-21 08:34:54 +02:00
1 parent 0b9846f55d
commit bd9bc78f4c
8 files changed
+148 -12

No files matched your search

+7 -7
View File
@@ -75,7 +75,7 @@ export async function fetchNetworkMetricsPlaywright(
console.log(`[qlik-pw] csrf-token capturé, ouverture du websocket Engine in-page…`);
const result = (await page.evaluate(
({ app, dim, mca, mqte, mnb, codes, token }: { app: string; dim: string; mca: string; mqte: string; mnb: string; codes: string[]; token: string }) =>
({ app, dim, mca, mqte, mnb, mcamag, mcouv, mmarge, mrupt, codes, token }: { app: string; dim: string; mca: string; mqte: string; mnb: string; mcamag: string; mcouv: string; mmarge: string; mrupt: string; codes: string[]; token: string }) =>
new Promise<InPageResult>((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}`;
@@ -112,7 +112,7 @@ export async function fetchNetworkMetricsPlaywright(
}
const obj = await rpc("CreateSessionObject", { qProp: { qInfo: { qType: "cf-net" }, qHyperCubeDef: {
qDimensions: [{ qLibraryId: dim }],
qMeasures: [{ qLibraryId: mca }, { qLibraryId: mqte }, { qLibraryId: mnb }],
qMeasures: [{ qLibraryId: mca }, { qLibraryId: mqte }, { qLibraryId: mnb }, { qLibraryId: mcamag }, { qLibraryId: mcouv }, { qLibraryId: mmarge }, { qLibraryId: mrupt }],
qInitialDataFetch: [],
} } }, doc);
const oh = (obj.qReturn as { qHandle: number }).qHandle;
@@ -120,12 +120,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<Array<string | number>> = [];
const PAGE = 2500;
const PAGE = 1250;
for (let top = 0; top < size; top += PAGE) {
const d = await rpc("GetHyperCubeData", { qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: 4, qHeight: PAGE }] }, oh);
const d = await rpc("GetHyperCubeData", { qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: 8, qHeight: PAGE }] }, oh);
const matrix = (d.qDataPages as Array<{ qMatrix?: Array<Array<{ qText?: string; qNum?: number }>> }>)?.[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]);
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]);
if (matrix.length < PAGE) break;
}
ws.close();
@@ -133,7 +133,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, 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, mrupt: cfg.measRuptPctId, codes: codeCentraux ?? [], token: csrfToken },
)) as InPageResult;
if (!result.ok) throw new Error(`[qlik-pw] ${result.error}`);
@@ -144,7 +144,7 @@ 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 });
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 });
}
console.log(`[qlik-pw] ${out.size} produits réseau (cube ${result.size})`);
return out;