mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat(grille): modal en courbe unique + retrait Quantité COMP (2025)
- Modal : les 12 barres remplacées par UNE seule courbe SVG des ventes réseau mensuelles (points + quantités affichées + axe des mois). - Sync : retrait de la mesure 'Quantité COMP' (année N-1) — trop lent à récupérer. Retour au cube [Article Code, Mois] à 5 mesures (Quantité N). Le détail mensuel couvre donc les mois de l'année en cours ; la tendance (régression) se calcule sur ces mois.
This commit is contained in:
2 files changed
+55
-48
No files matched your search
@@ -291,13 +291,6 @@ const TREND_COLOR: Record<NetworkTrend["direction"], string> = {
|
||||
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 <div className="text-center text-[12px]" style={{ color: "var(--text-secondary)" }}>-</div>;
|
||||
@@ -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 (
|
||||
<div className="mt-3 w-full overflow-x-auto">
|
||||
<svg viewBox={`0 0 ${W} ${H}`} className="w-full" style={{ minWidth: 380 }} role="img" aria-label="Ventes réseau mensuelles">
|
||||
{/* ligne de base */}
|
||||
<line x1={padL} y1={padT + plotH} x2={padL + plotW} y2={padT + plotH} stroke="var(--border)" strokeWidth={1} />
|
||||
{/* aire douce sous la courbe */}
|
||||
{n > 1 && <polygon points={areaPts} fill={color} opacity={0.08} />}
|
||||
{/* courbe */}
|
||||
{n > 1 && <polyline points={linePts} fill="none" stroke={color} strokeWidth={2} strokeLinejoin="round" strokeLinecap="round" />}
|
||||
{values.map((v, i) => (
|
||||
<g key={labels[i]}>
|
||||
<circle cx={x(i)} cy={y(v)} r={2.6} fill={color} />
|
||||
{/* quantité au-dessus du point */}
|
||||
<text x={x(i)} y={y(v) - 6} textAnchor="middle" fontSize={9} fontWeight={700} fill="var(--text-primary)">
|
||||
{Math.round(v).toLocaleString("fr-FR")}
|
||||
</text>
|
||||
{/* mois sous l'axe */}
|
||||
<text x={x(i)} y={H - 10} textAnchor="middle" fontSize={8} fill="var(--text-muted)">
|
||||
{fmtMonthShort(labels[i])}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 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 (
|
||||
<DialogContent className="max-w-md" style={{ background: "var(--bg-surface)", border: "1px solid var(--border)" }} onInteractOutside={onClose}>
|
||||
@@ -358,23 +394,7 @@ function NetworkMonthlyModal({ row, onClose }: { row: ProductRow; onClose: () =>
|
||||
Pas encore de détail mensuel pour ce produit.<br />Relancez un Sync Qlik pour le remplir.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5 mt-2 max-h-[60vh] overflow-y-auto pr-1">
|
||||
{labels.map((lab, i) => {
|
||||
const v = values[i];
|
||||
const w = Math.max(2, (v / max) * 100);
|
||||
return (
|
||||
<div key={lab} className="flex items-center gap-2">
|
||||
<span className="w-[64px] text-[11px] tabular-nums shrink-0" style={{ color: "var(--text-muted)" }}>{fmtMonthYYYYMM(lab)}</span>
|
||||
<div className="flex-1 h-4 rounded overflow-hidden" style={{ background: "var(--bg-elevated)" }}>
|
||||
<div className="h-full rounded" style={{ width: `${w}%`, background: "var(--accent)" }} />
|
||||
</div>
|
||||
<span className="w-[52px] text-right text-[12px] font-bold tabular-nums shrink-0" style={{ color: "var(--text-primary)" }}>
|
||||
{Math.round(v).toLocaleString("fr-FR")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<NetworkLineChart labels={labels} values={values} color={color} />
|
||||
)}
|
||||
</DialogContent>
|
||||
);
|
||||
|
||||
@@ -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<InPageResult>((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<string, number>,
|
||||
onCode15Retry?: () => void,
|
||||
): Promise<number> => {
|
||||
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<Array<{ qText?: string; qNum?: number }>> }>)?.[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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user