mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-15 19:28:52 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
af93e362e0
commit
524faf4a7a
10 files changed
+32
-303
No files matched your search
@@ -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<string, unknown>) ?? {}).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<string, unknown>) ?? {}).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;
|
||||
|
||||
@@ -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<nu
|
||||
caParMagasinReseau: String(m.caParMagasinReseau),
|
||||
couvertureStockReseau: String(m.couvertureStockReseau),
|
||||
margePctReseau: String(m.margePctReseau),
|
||||
rupturePctReseau: String(m.rupturePctReseau),
|
||||
periode: m.periode ?? null,
|
||||
fetchedAt: now,
|
||||
}));
|
||||
@@ -84,7 +81,6 @@ export async function upsertNetworkMetrics(metrics: NetworkMetric[]): Promise<nu
|
||||
caParMagasinReseau: sql`excluded.ca_par_magasin_reseau`,
|
||||
couvertureStockReseau: sql`excluded.couverture_stock_reseau`,
|
||||
margePctReseau: sql`excluded.marge_pct_reseau`,
|
||||
rupturePctReseau: sql`excluded.rupture_pct_reseau`,
|
||||
periode: sql`excluded.periode`,
|
||||
fetchedAt: sql`excluded.fetched_at`,
|
||||
},
|
||||
|
||||
@@ -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, 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 }) =>
|
||||
({ 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<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}`;
|
||||
@@ -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<string> => {
|
||||
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<Array<string | number>> = [];
|
||||
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<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, 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;
|
||||
|
||||
Reference in new issue
Block a user