diff --git a/src/features/grid/components/heatmap-grid.tsx b/src/features/grid/components/heatmap-grid.tsx index 594f281..5d54969 100644 --- a/src/features/grid/components/heatmap-grid.tsx +++ b/src/features/grid/components/heatmap-grid.tsx @@ -291,13 +291,6 @@ const TREND_COLOR: Record = { flat: "#94a3b8", }; -function fmtMonthYYYYMM(lab: string): string { - const [y, m] = lab.split("-"); - const names = ["janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."]; - const idx = Number(m) - 1; - return `${names[idx] ?? m} ${(y ?? "").slice(2)}`; -} - /** Sparkline compacte 12 mois + flèche, teintée selon la tendance. */ function TrendSparkline({ trend }: { trend: NetworkTrend }) { if (!trend.hasData) return
-
; @@ -330,11 +323,54 @@ function TrendSparkline({ trend }: { trend: NetworkTrend }) { ); } -/** Modal : quantités vendues réseau sur les 12 derniers mois, mois par mois. */ +/** "2026-01" → "01/26" pour un axe compact. */ +function fmtMonthShort(lab: string): string { + const [y, m] = lab.split("-"); + return `${m ?? lab}/${(y ?? "").slice(2)}`; +} + +/** Courbe unique des ventes réseau mensuelles (SVG). */ +function NetworkLineChart({ labels, values, color }: { labels: string[]; values: number[]; color: string }) { + const W = 460, H = 200; + const padL = 10, padR = 10, padT = 22, padB = 30; + const plotW = W - padL - padR, plotH = H - padT - padB; + const n = values.length; + const maxV = Math.max(...values, 1); + const x = (i: number) => (n > 1 ? padL + (i / (n - 1)) * plotW : padL + plotW / 2); + const y = (v: number) => padT + plotH - (v / maxV) * plotH; + const linePts = values.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" "); + const areaPts = `${padL},${padT + plotH} ${linePts} ${(padL + plotW).toFixed(1)},${(padT + plotH).toFixed(1)}`; + return ( +
+ + {/* ligne de base */} + + {/* aire douce sous la courbe */} + {n > 1 && } + {/* courbe */} + {n > 1 && } + {values.map((v, i) => ( + + + {/* quantité au-dessus du point */} + + {Math.round(v).toLocaleString("fr-FR")} + + {/* mois sous l'axe */} + + {fmtMonthShort(labels[i])} + + + ))} + +
+ ); +} + +/** Modal : quantités vendues réseau sur les 12 derniers mois, en une seule courbe. */ function NetworkMonthlyModal({ row, onClose }: { row: ProductRow; onClose: () => void }) { const trend = computeNetworkTrend(row.qteReseauByMonth); const { values, labels, direction, pct } = trend; - const max = Math.max(...values, 1); const color = TREND_COLOR[direction]; return ( @@ -358,23 +394,7 @@ function NetworkMonthlyModal({ row, onClose }: { row: ProductRow; onClose: () => Pas encore de détail mensuel pour ce produit.
Relancez un Sync Qlik pour le remplir. ) : ( -
- {labels.map((lab, i) => { - const v = values[i]; - const w = Math.max(2, (v / max) * 100); - return ( -
- {fmtMonthYYYYMM(lab)} -
-
-
- - {Math.round(v).toLocaleString("fr-FR")} - -
- ); - })} -
+ )} ); diff --git a/src/lib/qlik-playwright.ts b/src/lib/qlik-playwright.ts index 7d5f09e..fe1022b 100644 --- a/src/lib/qlik-playwright.ts +++ b/src/lib/qlik-playwright.ts @@ -108,10 +108,6 @@ export async function fetchNetworkMetricsPlaywright( // Activé par défaut ; désactivable via QLIK_MONTH_DIM=0 (revient à l'itération par mois). const qlikMonthDim = !["0", "false", "no", "off"].includes((process.env.QLIK_MONTH_DIM ?? "").trim().toLowerCase()); const qlikMoisDimId = (process.env.QLIK_MOIS_DIM_ID ?? "pfGAwTs").trim(); - // Mesure "Quantité COMP" (année N-1) : complète les mois de l'année précédente pour - // obtenir 12 mois glissants (Quantité N ne couvre que l'année en cours). - const qlikMeasQteCompId = (process.env.QLIK_MEAS_QTE_COMP_ID ?? "96862880-76cd-4957-bf25-b96901f2ac5f").trim(); - const yearN = new Date().getFullYear(); console.log(`[qlik-pw] tuning settle=${qlikSettleMs}ms code15Attempts=${qlikCode15MaxAttempts} batchFallbacks=${qlikArticleBatchFallbacks.join(">")} selectAllCodes=${qlikSelectAllCodes} monthDim=${qlikMonthDim}`); const browser = await getBrowser(); @@ -153,7 +149,7 @@ export async function fetchNetworkMetricsPlaywright( })); const result = (await page.evaluate( - ({ app, dim, mca, mqte, mnb, mcamag, mmarge, codes, token, monthlyPayload, noDateMode, qlikSettleMs, qlikCode15MaxAttempts, qlikArticleBatchFallbacks, selectAllCodes, monthDim, moisDimId, mqteComp, yearN }: { + ({ app, dim, mca, mqte, mnb, mcamag, mmarge, codes, token, monthlyPayload, noDateMode, qlikSettleMs, qlikCode15MaxAttempts, qlikArticleBatchFallbacks, selectAllCodes, monthDim, moisDimId }: { app: string; dim: string; mca: string; mqte: string; mnb: string; mcamag: string; mmarge: string; codes: string[]; token: string; monthlyPayload: Array<{ label: string; dateDebut: string; dateFin: string; serials: number[] }>; @@ -164,8 +160,6 @@ export async function fetchNetworkMetricsPlaywright( selectAllCodes: boolean; monthDim: boolean; moisDimId: string; - mqteComp: string; - yearN: number; }) => new Promise((resolve) => { const loc = (window as unknown as { location: Location }).location; @@ -367,16 +361,16 @@ export async function fetchNetworkMetricsPlaywright( // Cube [Article Code, Mois] × 5 mesures. Chaque ligne = (code, mois, ca, // qte, nbMag, caMag, marge). Le "Mois" est au format "YYYYMM" → "YYYY-MM". const normMois = (m: string): string => (/^\d{6}$/.test(m) ? m.slice(0, 4) + "-" + m.slice(4) : m); - const PAGEM = 1200; // 8 colonnes × 1200 = 9600 cellules < 10000 (limite Qlik) + const PAGEM = 1400; // 7 colonnes × 1400 = 9800 cellules < 10000 (limite Qlik) const fetchMonthCube = async ( - onRow: (code: string, mois: string, ca: number, qte: number, nbMag: number, caMag: number, marge: number, qteComp: number) => void, + onRow: (code: string, mois: string, ca: number, qte: number, nbMag: number, caMag: number, marge: number) => void, timings?: Record, onCode15Retry?: () => void, ): Promise => { const { value: obj } = await timed("createCube", "CreateSessionObject", () => rpcWithRetry("createCube", "CreateSessionObject", { qProp: { qInfo: { qType: "cf-net-mois" }, qHyperCubeDef: { qDimensions: [{ qLibraryId: dim }, { qLibraryId: moisDimId }], - qMeasures: [{ qLibraryId: mca }, { qLibraryId: mqte }, { qLibraryId: mnb }, { qLibraryId: rCamag }, { qLibraryId: rMarge }, { qLibraryId: mqteComp }], - qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 8, qHeight: PAGEM }], + qMeasures: [{ qLibraryId: mca }, { qLibraryId: mqte }, { qLibraryId: mnb }, { qLibraryId: rCamag }, { qLibraryId: rMarge }], + qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 7, qHeight: PAGEM }], } } }, doc, onCode15Retry), timings); const qReturn = obj.qReturn as { qHandle: number; qGenericId?: string; qId?: string }; const cube = { handle: qReturn.qHandle, id: String(qReturn.qGenericId ?? qReturn.qId ?? "") }; @@ -386,7 +380,7 @@ export async function fetchNetworkMetricsPlaywright( onRow( String(r[0]?.qText ?? ""), String(r[1]?.qText ?? ""), 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, + Number(r[5]?.qNum) || 0, Number(r[6]?.qNum) || 0, ); } return matrix.length; @@ -399,7 +393,7 @@ export async function fetchNetworkMetricsPlaywright( const firstPage = hc.qDataPages?.find((p) => (p.qArea?.qTop ?? 0) === 0); if (firstPage?.qMatrix?.length) { got += process(firstPage.qMatrix); top = PAGEM; } for (; top < size; top += PAGEM) { - const { value: d } = await timed("getCubeData", "GetHyperCubeData", () => rpcWithRetry("getCubeData", "GetHyperCubeData", { qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: 8, qHeight: PAGEM }] }, oh, onCode15Retry), timings); + const { value: d } = await timed("getCubeData", "GetHyperCubeData", () => rpcWithRetry("getCubeData", "GetHyperCubeData", { qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: 7, qHeight: PAGEM }] }, oh, onCode15Retry), timings); const matrix = (d.qDataPages as Array<{ qMatrix?: Array> }>)?.[0]?.qMatrix ?? []; if (!matrix.length) break; got += process(matrix); @@ -438,17 +432,12 @@ export async function fetchNetworkMetricsPlaywright( await timed("selectCode", "SelectValues", () => rpc("SelectValues", { qFieldValues: batch.map((c) => ({ qText: c })), qToggleMode: false, qSoftLock: true }, fh), timings); await sleep(qlikSettleMs); } - const got = await timed("cube", "batch", () => fetchMonthCube((code, mois, ca, qte, nbMag, caMag, marge, qteComp) => { + const got = await timed("cube", "batch", () => fetchMonthCube((code, mois, ca, qte, nbMag, caMag, marge) => { if (!code || code === "-") return; - // Total réseau = année N (mesures "N" : CA/Qté/Marge). Non gonflé. out.push([code, ca, qte, nbMag, caMag, marge]); - // Détail mensuel 12 mois glissants : mois de l'année N → Quantité N, - // mois de l'année N-1 → Quantité COMP. const mm = normMois(mois); - const y = parseInt(mm.slice(0, 4), 10); - const qtyMonth = y === yearN ? qte : qteComp; (monthlyByCode[code] ??= {}); - monthlyByCode[code][mm] = (monthlyByCode[code][mm] ?? 0) + qtyMonth; + monthlyByCode[code][mm] = (monthlyByCode[code][mm] ?? 0) + qte; }, timings, () => { code15Retries++; }), timings); aggBatches++; aggCode15Retries += code15Retries; @@ -714,8 +703,6 @@ export async function fetchNetworkMetricsPlaywright( selectAllCodes: qlikSelectAllCodes, monthDim: qlikMonthDim, moisDimId: qlikMoisDimId, - mqteComp: qlikMeasQteCompId, - yearN, }, )) as InPageResult;