mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
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:
1 parent
0b9846f55d
commit
bd9bc78f4c
8 files changed
+148
-12
No files matched your search
@@ -102,10 +102,22 @@ async function main() {
|
||||
"ca_reseau" numeric(16, 2),
|
||||
"qte_reseau" numeric(14, 2),
|
||||
"nb_magasins_reseau" integer,
|
||||
"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()
|
||||
);
|
||||
`);
|
||||
// Colonnes ajoutées après coup (DB existante) — CREATE IF NOT EXISTS ne les pose pas.
|
||||
await tempPool.query(`
|
||||
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);
|
||||
`);
|
||||
console.log("[DB Init] Table qlik_network_metrics is verified/created.");
|
||||
|
||||
await tempPool.end();
|
||||
|
||||
@@ -117,6 +117,14 @@ export const qlikNetworkMetrics = pgTable("qlik_network_metrics", {
|
||||
qteReseau: numeric("qte_reseau", { precision: 14, scale: 2 }),
|
||||
/** Nombre de magasins travaillant le produit (sur ~270) */
|
||||
nbMagasinsReseau: integer("nb_magasins_reseau"),
|
||||
/** CA moyen par magasin (productivité normalisée présence) */
|
||||
caParMagasinReseau: numeric("ca_par_magasin_reseau", { precision: 16, scale: 2 }),
|
||||
/** Couverture de stock en quantité (rotation) */
|
||||
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 */
|
||||
|
||||
@@ -481,6 +481,10 @@ async function enrichWithNetworkMetrics(productMap: Map<string, ProductRow>): Pr
|
||||
product.caReseau = m.caReseau;
|
||||
product.qteReseau = m.qteReseau;
|
||||
product.nbMagasinsReseau = m.nbMagasinsReseau;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ const GridRow = React.memo(({ virtualRow, row, rowHeight, isSelected, columnVisi
|
||||
>
|
||||
{row.getVisibleCells().map((cell: Cell<ProductRow, unknown>) => {
|
||||
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 === "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 === "rupturePctReseau" || cell.column.id === "tauxPresenceReseau" || cell.column.id === "gamme";
|
||||
const size = cell.column.getSize();
|
||||
return (
|
||||
<td
|
||||
@@ -560,6 +560,68 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "caParMagasinReseau",
|
||||
header: () => <div className="text-center w-full">CA / Mag<br/><span className="text-[9px] opacity-60">Réseau</span></div>,
|
||||
size: 85,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue<number | undefined>();
|
||||
return (
|
||||
<div className="text-center tabular-nums text-[12px] font-bold" style={{ color: "var(--text-secondary)" }}>
|
||||
{val != null ? val.toLocaleString("fr-FR", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }) : "-"}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "couvertureStockReseau",
|
||||
header: () => <div className="text-center w-full">Couv.<br/><span className="text-[9px] opacity-60">Stock</span></div>,
|
||||
size: 70,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue<number | undefined>();
|
||||
if (val == null) return <div className="text-center text-[12px]" style={{ color: "var(--text-secondary)" }}>-</div>;
|
||||
// Couverture basse = rotation rapide = bon signal d'achat
|
||||
const color = val <= 2 ? "text-emerald-500" : val <= 4 ? "text-amber-500" : "text-rose-500";
|
||||
return (
|
||||
<div className={cn("text-center font-bold text-[12px] tabular-nums", color)}>
|
||||
{val.toLocaleString("fr-FR", { maximumFractionDigits: 1 })}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "margePctReseau",
|
||||
header: () => <div className="text-center w-full">Marge %<br/><span className="text-[9px] opacity-60">Réseau</span></div>,
|
||||
size: 75,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue<number | undefined>();
|
||||
if (val == null) return <div className="text-center text-[12px]" style={{ color: "var(--text-secondary)" }}>-</div>;
|
||||
const pct = Math.abs(val) <= 1 ? val * 100 : val;
|
||||
const color = pct >= 30 ? "text-emerald-500" : pct >= 15 ? "text-amber-500" : "text-rose-500";
|
||||
return (
|
||||
<div className={cn("text-center font-bold text-[12px] tabular-nums", color)}>
|
||||
{pct.toLocaleString("fr-FR", { maximumFractionDigits: 1 })}%
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "rupturePctReseau",
|
||||
header: () => <div className="text-center w-full">Rupt. %<br/><span className="text-[9px] opacity-60">Réseau</span></div>,
|
||||
size: 75,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue<number | undefined>();
|
||||
if (val == null) return <div className="text-center text-[12px]" style={{ color: "var(--text-secondary)" }}>-</div>;
|
||||
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 (
|
||||
<div className={cn("text-center font-bold text-[12px] tabular-nums", color)}>
|
||||
{pct.toLocaleString("fr-FR", { maximumFractionDigits: 1 })}%
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
...MONTHS_12.map((monthKey) => ({
|
||||
id: `month_${monthKey}`,
|
||||
header: () => <div className="text-center w-full">{formatMonthLabel(monthKey)}</div>,
|
||||
@@ -778,7 +840,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
|
||||
<tr key={headerGroup.id} className="flex w-full">
|
||||
{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 === "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 === "rupturePctReseau" || header.column.id === "tauxPresenceReseau" || header.column.id === "gamme";
|
||||
const size = header.getSize();
|
||||
return (
|
||||
<th
|
||||
|
||||
+29
-3
@@ -48,6 +48,10 @@ export interface QlikConfig {
|
||||
measCaId: string; // master measure "CA N"
|
||||
measQteId: string; // master measure "Quantite N"
|
||||
measNbMagId: string; // master measure "Magasin Ventes Nb N"
|
||||
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;
|
||||
}
|
||||
@@ -67,6 +71,12 @@ export function getQlikConfig(): QlikConfig {
|
||||
measCaId: process.env.QLIK_MEAS_CA_ID ?? "JhqJ",
|
||||
measQteId: process.env.QLIK_MEAS_QTE_ID ?? "41516861-5997-4635-8187-3643a2bde422",
|
||||
measNbMagId: process.env.QLIK_MEAS_NBMAG_ID ?? "yNBLjc",
|
||||
// Master GUIDs de l'app "Magasins Vision Consolidée" (9872ee6e). Si l'engine
|
||||
// les refuse (qId court attendu), override via env après discovery.
|
||||
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"),
|
||||
};
|
||||
@@ -77,6 +87,14 @@ export interface NetworkMetric {
|
||||
caReseau: number;
|
||||
qteReseau: number;
|
||||
nbMagasinsReseau: number;
|
||||
/** CA moyen par magasin (productivité normalisée présence) */
|
||||
caParMagasinReseau: number;
|
||||
/** Couverture de stock en quantité (rotation : bas = tourne vite) */
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -229,8 +247,12 @@ function hyperCubeDef(cfg: QlikConfig, height: number) {
|
||||
{ qLibraryId: cfg.measCaId },
|
||||
{ qLibraryId: cfg.measQteId },
|
||||
{ qLibraryId: cfg.measNbMagId },
|
||||
{ qLibraryId: cfg.measCaMagId },
|
||||
{ qLibraryId: cfg.measCouvQteId },
|
||||
{ qLibraryId: cfg.measMargePctId },
|
||||
{ qLibraryId: cfg.measRuptPctId },
|
||||
],
|
||||
qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 4, qHeight: height }],
|
||||
qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 8, qHeight: height }],
|
||||
qSuppressZero: false,
|
||||
qSuppressMissing: true,
|
||||
},
|
||||
@@ -253,7 +275,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 = 2500; // 4 colonnes * 2500 = 10000 cellules max
|
||||
const pageHeight = 1250; // 8 colonnes * 1250 = 10000 cellules max
|
||||
const createRes = await conn.rpc("CreateSessionObject", { qProp: hyperCubeDef(cfg, pageHeight) }, docHandle);
|
||||
const objHandle = (((createRes.qReturn as Record<string, unknown>) ?? {}).qHandle as number);
|
||||
|
||||
@@ -261,7 +283,7 @@ export async function fetchNetworkMetrics(
|
||||
for (;;) {
|
||||
const dataRes = await conn.rpc(
|
||||
"GetHyperCubeData",
|
||||
{ qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: 4, qHeight: pageHeight }] },
|
||||
{ qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: 8, qHeight: pageHeight }] },
|
||||
objHandle,
|
||||
);
|
||||
const pages = (dataRes.qDataPages as Array<{ qMatrix?: Matrix }>) ?? [];
|
||||
@@ -277,6 +299,10 @@ export async function fetchNetworkMetrics(
|
||||
caReseau: Number(row[1]?.qNum ?? 0) || 0,
|
||||
qteReseau: Number(row[2]?.qNum ?? 0) || 0,
|
||||
nbMagasinsReseau: Number(row[3]?.qNum ?? 0) || 0,
|
||||
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;
|
||||
|
||||
@@ -15,6 +15,10 @@ export interface NetworkMetricCached {
|
||||
caReseau: number;
|
||||
qteReseau: number;
|
||||
nbMagasinsReseau: number;
|
||||
caParMagasinReseau: number;
|
||||
couvertureStockReseau: number;
|
||||
margePctReseau: number;
|
||||
rupturePctReseau: number;
|
||||
fetchedAt: string | null;
|
||||
}
|
||||
|
||||
@@ -36,6 +40,10 @@ export async function getNetworkMetricsByCodeCentrale(
|
||||
caReseau: Number(r.caReseau ?? 0) || 0,
|
||||
qteReseau: Number(r.qteReseau ?? 0) || 0,
|
||||
nbMagasinsReseau: Number(r.nbMagasinsReseau ?? 0) || 0,
|
||||
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,
|
||||
});
|
||||
}
|
||||
@@ -51,6 +59,10 @@ export async function upsertNetworkMetrics(metrics: NetworkMetric[]): Promise<nu
|
||||
caReseau: String(m.caReseau),
|
||||
qteReseau: String(m.qteReseau),
|
||||
nbMagasinsReseau: m.nbMagasinsReseau,
|
||||
caParMagasinReseau: String(m.caParMagasinReseau),
|
||||
couvertureStockReseau: String(m.couvertureStockReseau),
|
||||
margePctReseau: String(m.margePctReseau),
|
||||
rupturePctReseau: String(m.rupturePctReseau),
|
||||
periode: m.periode ?? null,
|
||||
fetchedAt: now,
|
||||
}));
|
||||
@@ -69,6 +81,10 @@ export async function upsertNetworkMetrics(metrics: NetworkMetric[]): Promise<nu
|
||||
caReseau: sql`excluded.ca_reseau`,
|
||||
qteReseau: sql`excluded.qte_reseau`,
|
||||
nbMagasinsReseau: sql`excluded.nb_magasins_reseau`,
|
||||
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, 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;
|
||||
|
||||
@@ -113,6 +113,14 @@ export interface ProductRow {
|
||||
caReseau?: number;
|
||||
qteReseau?: number;
|
||||
nbMagasinsReseau?: number;
|
||||
/** CA moyen par magasin réseau (productivité normalisée présence) */
|
||||
caParMagasinReseau?: number;
|
||||
/** Couverture de stock quantité réseau (rotation : bas = tourne vite) */
|
||||
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) */
|
||||
|
||||
Reference in new issue
Block a user