mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
Qlik: ne plus effacer Periode — c'est elle qui donne son sens a Type_Cal
Le log de production tranche : sans selection de Periode, les master measures sont MUETTES. Les passes annuelles renvoyaient bien 270 056 lignes, mais toutes mesures nulles — d'ou « 0 point mensuel ajoute » puis le refus des 12 mois. choisirPeriode() jugeait chaque valeur de Periode SANS annee selectionnee. « Annee a date » y parait couvrir 6/12 mois, donc etait rejetee, donc Periode etait effacee — et tout mourait. Or avec Annee=2025 selectionnee, la meme valeur donne l'annee 2025 COMPLETE (l'app affiche « Du 02/01/2025 au 31/12/2025 »), et avec Annee=2026 elle s'arrete au mois courant. L'union des deux passes fait bien 12 mois. - La couverture d'une Periode est desormais mesuree comme l'UNION des passes annuelles, exactement comme monthDimPath les executera. - La meilleure valeur est CONSERVEE meme partielle ; seule une couverture nulle justifie d'effacer. - Annees traitees la plus recente d'abord (usage de l'app), arret des que la fenetre est complete. - Diagnostics : mois gagnes / encore manquants apres chaque passe, et liste des mois manquants dans les messages d'echec. - Une passe ne peut plus ecraser un mois servi par un zero. Deux defauts prouves par le meme log, cote agregation directe : - la master « Quantite N » morte vidait TOUT l'hypercube (4 expressions valides a 6 mois, cube complet a 0 ligne) : elle est desormais sondee et omise si morte ; - ventilee par Mois, Sum(quantite) totalise 179 574 616 contre 33 945 285 sans dimension (x5,3) : la dimension Mois passe par le pont de periodes et demultiplie les faits. Ce chemin est refuse plutot que d'ecrire des ventes reseau fausses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
This commit is contained in:
1 file changed
+220
-42
+220
-42
@@ -562,7 +562,17 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
onRow: (code: string, mois: string, ca: number, qte: number, nbMag: number, marge: number, qteMaster: number) => void,
|
||||
timings?: Record<string, number>,
|
||||
onCode15Retry?: () => void,
|
||||
/**
|
||||
* Inclure la master « Quantité N » (colonne de calibrage) ?
|
||||
*
|
||||
* ⚠️ Elle ne coûte rien quand elle vit, mais **annule tout le cube**
|
||||
* quand elle est morte : mesuré en production, 4 expressions valides
|
||||
* à 6 mois chacune et le cube complet à **0 ligne** après 25 s de
|
||||
* calcul. L'appelant ne l'ajoute donc qu'après l'avoir sondée.
|
||||
*/
|
||||
avecMaster = true,
|
||||
): Promise<number> => {
|
||||
const largeur = avecMaster ? 7 : 6;
|
||||
const { value: obj } = await timed("createCubeExpr", "CreateSessionObject", () => rpcWithRetry("createCubeExpr", "CreateSessionObject", { qProp: { qInfo: { qType: "cf-net-mois-expr" }, qHyperCubeDef: {
|
||||
qDimensions: [{ qLibraryId: dim }, { qLibraryId: moisDimId }],
|
||||
qMeasures: [
|
||||
@@ -572,9 +582,9 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
{ qDef: { qDef: eMarge } },
|
||||
// Master « Quantité N », uniquement pour le calibrage : sur les
|
||||
// mois de l'année en cours les deux doivent coïncider.
|
||||
{ qLibraryId: rQte },
|
||||
...(avecMaster ? [{ qLibraryId: rQte }] : []),
|
||||
],
|
||||
qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 7, qHeight: PAGEX }],
|
||||
qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: largeur, qHeight: PAGEX }],
|
||||
} } }, 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 ?? "") };
|
||||
@@ -584,7 +594,8 @@ 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[4]?.qNum) || 0, Number(r[5]?.qNum) || 0,
|
||||
avecMaster ? Number(r[6]?.qNum) || 0 : 0,
|
||||
);
|
||||
}
|
||||
return matrix.length;
|
||||
@@ -597,7 +608,7 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
const firstPage = hc.qDataPages?.find((pg) => (pg.qArea?.qTop ?? 0) === 0);
|
||||
if (firstPage?.qMatrix?.length) { got += process(firstPage.qMatrix); top = PAGEX; }
|
||||
for (; top < size; top += PAGEX) {
|
||||
const { value: d } = await timed("getCubeData", "GetHyperCubeData", () => rpcWithRetry("getCubeData", "GetHyperCubeData", { qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: 7, qHeight: PAGEX }] }, oh, onCode15Retry), timings);
|
||||
const { value: d } = await timed("getCubeData", "GetHyperCubeData", () => rpcWithRetry("getCubeData", "GetHyperCubeData", { qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: largeur, qHeight: PAGEX }] }, 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);
|
||||
@@ -906,7 +917,28 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
return false;
|
||||
};
|
||||
|
||||
const choisirPeriode = async (): Promise<number> => {
|
||||
/**
|
||||
* Choisit la valeur de `Période` sous laquelle l'extraction aura lieu.
|
||||
*
|
||||
* ⚠️ Correction majeure. La version précédente mesurait la couverture
|
||||
* d'une `Période` **sans sélection d'année**, puis effaçait `Période`
|
||||
* si aucune valeur ne couvrait les 12 mois. Or :
|
||||
*
|
||||
* - sans `Période`, `Type_Cal` ne résout rien : « Quantité N » est
|
||||
* morte partout (mesuré : couverture 0/12). Les passes annuelles
|
||||
* renvoyaient bien 270 056 lignes, mais **toutes mesures nulles** —
|
||||
* d'où « 0 point mensuel ajouté » et le refus final ;
|
||||
* - « Année à date » ne vaut « janvier → aujourd'hui » que pour
|
||||
* l'année EN COURS. Avec `Année=2025` sélectionnée, la même valeur
|
||||
* donne l'année 2025 COMPLÈTE — c'est exactement ce que montre
|
||||
* l'app : « Période d'analyse : Du 02/01/2025 au 31/12/2025 ».
|
||||
*
|
||||
* La couverture d'une `Période` ne veut donc rien dire hors du contexte
|
||||
* d'année dans lequel elle sera utilisée. On la mesure désormais comme
|
||||
* l'**union des passes annuelles** — 2025 puis 2026 — c'est-à-dire
|
||||
* exactement ce que fera `monthDimPath`.
|
||||
*/
|
||||
const choisirPeriode = async (annees: number[]): Promise<number> => {
|
||||
const fenetre = new Set(monthlyPayload.map((m) => m.label));
|
||||
let lo: { qHandle: number; qGenericId?: string; qId?: string } | null = null;
|
||||
try {
|
||||
@@ -953,16 +985,43 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Couverture d'une `Période` telle qu'elle sera RÉELLEMENT
|
||||
* exploitée : une passe par année, comme `monthDimPath`, puis
|
||||
* union. C'est le seul chiffre qui a un sens ici — mesurer
|
||||
* sans année revient à juger « Année à date » sur la seule
|
||||
* année en cours (6/12) et à la rejeter à tort.
|
||||
*/
|
||||
const mesurerCouvertureUnion = async (): Promise<string[]> => {
|
||||
if (yfh === -1) return mesurerCouverture();
|
||||
const union = new Set<string>();
|
||||
for (const annee of annees) {
|
||||
await rpc("Clear", {}, yfh);
|
||||
await sleep(qlikSettleMs);
|
||||
await rpc("SelectValues", {
|
||||
qFieldValues: [{ qNum: annee, qText: String(annee) }],
|
||||
qToggleMode: false,
|
||||
qSoftLock: true,
|
||||
}, yfh);
|
||||
await sleep(qlikSettleMs);
|
||||
const mois = await mesurerCouverture();
|
||||
diag(" Année=" + annee + " → " + mois.length + " mois " + JSON.stringify(mois));
|
||||
for (const m of mois) union.add(m);
|
||||
}
|
||||
// L'année ne doit pas rester sélectionnée : chaque passe
|
||||
// pose la sienne.
|
||||
await rpc("Clear", {}, yfh);
|
||||
await sleep(qlikSettleMs);
|
||||
return [...union].sort();
|
||||
};
|
||||
|
||||
// CANDIDAT ZÉRO : aucune sélection de « Période ».
|
||||
//
|
||||
// L'app pose une valeur par défaut à l'ouverture (observé :
|
||||
// « Période:1/4 » alors que nous n'avions rien sélectionné).
|
||||
// C'est cette valeur héritée qui bornait les mesures — et c'est
|
||||
// sans elle qu'un utilisateur voit 2024, 2025 et 2026 mois par
|
||||
// mois dans l'app. On teste donc l'état libre EN PREMIER.
|
||||
// Conservé pour le diagnostic, mais on sait maintenant qu'il
|
||||
// renvoie 0/12 : sans `Période`, `Type_Cal` ne résout rien.
|
||||
await rpc("ClearField", { qFieldName: "Période" }, doc).catch(() => rpc("Clear", {}, lo!.qHandle));
|
||||
await sleep(qlikSettleMs);
|
||||
const sansSelection = await mesurerCouverture();
|
||||
const sansSelection = await mesurerCouvertureUnion();
|
||||
diag(" SANS sélection de Période → " + sansSelection.length + "/" + fenetre.size + " mois " + JSON.stringify(sansSelection));
|
||||
if (sansSelection.length >= fenetre.size) {
|
||||
diag("Période laissée LIBRE : les mesures couvrent déjà les " + fenetre.size + " mois");
|
||||
@@ -979,8 +1038,8 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
}, lo.qHandle);
|
||||
await sleep(qlikSettleMs);
|
||||
|
||||
const couverts = await mesurerCouverture();
|
||||
diag(" Période « " + v.texte + " » → " + couverts.length + "/" + fenetre.size + " mois de la fenêtre " + JSON.stringify(couverts));
|
||||
const couverts = await mesurerCouvertureUnion();
|
||||
diag(" Période « " + v.texte + " » → " + couverts.length + "/" + fenetre.size + " mois de la fenêtre (union des années) " + JSON.stringify(couverts));
|
||||
if (!meilleure || couverts.length > meilleure.couverts.length) {
|
||||
meilleure = { texte: v.texte, elem: v.elem, couverts };
|
||||
}
|
||||
@@ -989,20 +1048,23 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
|
||||
const couverture = meilleure ? meilleure.couverts.length : 0;
|
||||
|
||||
// Une `Période` partielle est PIRE que pas de sélection : elle
|
||||
// restreint les faits au contexte choisi (« Année à date » ne
|
||||
// porte que 2026-01→06) et ampute d'autant l'agrégation
|
||||
// directe, qui elle sait lire tout l'historique. On ne garde
|
||||
// donc la sélection QUE si elle couvre toute la fenêtre.
|
||||
if (!meilleure || couverture < fenetre.size) {
|
||||
// On garde la MEILLEURE valeur, même partielle.
|
||||
//
|
||||
// L'inverse a été essayé et s'est retourné contre nous :
|
||||
// effacer `Période` ne « libère » pas les faits, elle rend les
|
||||
// master measures muettes (0/12 mesuré). Une couverture
|
||||
// partielle reste strictement meilleure que rien ; seul un
|
||||
// zéro absolu justifie d'effacer et de tenter l'agrégation
|
||||
// directe.
|
||||
if (!meilleure || couverture === 0) {
|
||||
await rpc("ClearField", { qFieldName: "Période" }, doc)
|
||||
.catch(() => rpc("Clear", {}, lo!.qHandle));
|
||||
await sleep(qlikSettleMs);
|
||||
diag(
|
||||
"aucune valeur de « Période » ne couvre les " + fenetre.size + " mois (meilleure = " +
|
||||
couverture + ") → sélection Période EFFACÉE pour ne pas amputer les faits",
|
||||
"aucune valeur de « Période » ne rend le moindre mois vivant → sélection Période " +
|
||||
"effacée, repli sur l'agrégation directe des faits",
|
||||
);
|
||||
return couverture;
|
||||
return 0;
|
||||
}
|
||||
|
||||
await rpc("SelectListObjectValues", {
|
||||
@@ -1012,7 +1074,10 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
qSoftLock: true,
|
||||
}, lo.qHandle);
|
||||
await sleep(qlikSettleMs);
|
||||
diag("Période retenue : « " + meilleure.texte + " » (" + couverture + "/" + fenetre.size + " mois)");
|
||||
diag(
|
||||
"Période retenue : « " + meilleure.texte + " » (" + couverture + "/" + fenetre.size +
|
||||
" mois en union des années" + (couverture < fenetre.size ? " — meilleure disponible" : "") + ")",
|
||||
);
|
||||
return couverture;
|
||||
} catch (e) {
|
||||
diag("choix de « Période » impossible : " + String((e as Error)?.message || e));
|
||||
@@ -1074,6 +1139,70 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* La master « Quantité N » produit-elle la moindre ligne sur `[Mois]`
|
||||
* dans l'état de sélection courant ? Même sonde que
|
||||
* `validerExpression`, mais par `qLibraryId`.
|
||||
*/
|
||||
const masterQteVivante = async (): Promise<boolean> => {
|
||||
let ret: { qHandle: number; qGenericId?: string; qId?: string } | null = null;
|
||||
try {
|
||||
const o = await rpc("CreateSessionObject", { qProp: {
|
||||
qInfo: { qType: "cf-test-master" },
|
||||
qHyperCubeDef: {
|
||||
qDimensions: [{ qLibraryId: moisDimId }],
|
||||
qMeasures: [{ qLibraryId: rQte }],
|
||||
qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 2, qHeight: 30 }],
|
||||
},
|
||||
} }, doc);
|
||||
ret = o.qReturn as { qHandle: number; qGenericId?: string; qId?: string };
|
||||
const lay = await rpc("GetLayout", {}, ret.qHandle);
|
||||
const hc = (lay.qLayout as { qHyperCube?: { qDataPages?: Array<{ qMatrix?: Array<Array<{ qNum?: number }>> }> } }).qHyperCube;
|
||||
let total = 0;
|
||||
for (const r of hc?.qDataPages?.[0]?.qMatrix ?? []) total += Number(r[1]?.qNum) || 0;
|
||||
diag(" master « Quantité N » (calibrage) → total=" + Math.round(total));
|
||||
return total > 0;
|
||||
} catch (e) {
|
||||
diag(" master « Quantité N » → ERREUR " + String((e as Error)?.message || e));
|
||||
return false;
|
||||
} finally {
|
||||
const id = String(ret?.qGenericId ?? ret?.qId ?? "");
|
||||
if (id) { try { await rpc("DestroySessionObject", { qId: id }, doc); } catch { /* noop */ } }
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Somme de `expr` VENTILÉE par la dimension Mois.
|
||||
*
|
||||
* Comparée au total sans dimension, elle dit si la dimension Mois
|
||||
* démultiplie les faits à travers le pont de périodes — auquel cas
|
||||
* l'agrégation directe est inexploitable.
|
||||
*/
|
||||
const totalVentileParMois = async (expr: string): Promise<number> => {
|
||||
let ret: { qHandle: number; qGenericId?: string; qId?: string } | null = null;
|
||||
try {
|
||||
const o = await rpc("CreateSessionObject", { qProp: {
|
||||
qInfo: { qType: "cf-ventil-mois" },
|
||||
qHyperCubeDef: {
|
||||
qDimensions: [{ qLibraryId: moisDimId }],
|
||||
qMeasures: [{ qDef: { qDef: expr } }],
|
||||
qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 2, qHeight: 200 }],
|
||||
},
|
||||
} }, doc);
|
||||
ret = o.qReturn as { qHandle: number; qGenericId?: string; qId?: string };
|
||||
const lay = await rpc("GetLayout", {}, ret.qHandle);
|
||||
const hc = (lay.qLayout as { qHyperCube?: { qDataPages?: Array<{ qMatrix?: Array<Array<{ qNum?: number }>> }> } }).qHyperCube;
|
||||
let total = 0;
|
||||
for (const r of hc?.qDataPages?.[0]?.qMatrix ?? []) total += Number(r[1]?.qNum) || 0;
|
||||
return total;
|
||||
} catch {
|
||||
return 0;
|
||||
} finally {
|
||||
const id = String(ret?.qGenericId ?? ret?.qId ?? "");
|
||||
if (id) { try { await rpc("DestroySessionObject", { qId: id }, doc); } catch { /* noop */ } }
|
||||
}
|
||||
};
|
||||
|
||||
const moisParExpressions = async (): Promise<boolean> => {
|
||||
const timings: Record<string, number> = {};
|
||||
const marqueOut = out.length;
|
||||
@@ -1168,6 +1297,34 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
const vNbMag = await validerExpression("magasins", exprNbMag);
|
||||
const vMarge = await validerExpression("marge", exprMarge);
|
||||
|
||||
// La master « Quantité N » n'est là que pour le calibrage, mais
|
||||
// une master morte suffit à vider tout l'hypercube. On la sonde
|
||||
// donc comme les autres, et on l'omet plutôt que de perdre
|
||||
// l'extraction entière pour une colonne de contrôle.
|
||||
const avecMaster = await masterQteVivante();
|
||||
if (!avecMaster) {
|
||||
diag(" master « Quantité N » morte sous cette sélection → colonne de calibrage retirée du cube");
|
||||
}
|
||||
|
||||
// ─── La dimension Mois démultiplie-t-elle les faits ? ────
|
||||
//
|
||||
// Le pont de périodes rattache une même ligne de fait à
|
||||
// plusieurs contextes. Ventilé par `Mois`, `Sum(quantite)` peut
|
||||
// donc dépasser le total sans dimension — mesuré : 179 574 616
|
||||
// contre 33 945 285, soit ×5,3. Écrire cela dans le cache
|
||||
// donnerait des ventes réseau cinq fois trop hautes : on refuse.
|
||||
const totalVentile = await totalVentileParMois(vQte);
|
||||
if (totalVentile > totalFenetre * 1.05) {
|
||||
diag(
|
||||
"ÉCHEC : ventilée par Mois, « " + vQte + " » totalise " + Math.round(totalVentile) +
|
||||
" contre " + Math.round(totalFenetre) + " sans dimension (×" +
|
||||
(totalFenetre > 0 ? (totalVentile / totalFenetre).toFixed(1) : "?") +
|
||||
"). La dimension Mois passe par le pont de périodes et démultiplie les faits — " +
|
||||
"l'agrégation directe donnerait des ventes réseau fausses, elle est refusée.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Seuls les mois de la fenêtre nous intéressent ; la dimension
|
||||
// Mois peut en exposer d'autres si une sélection déborde.
|
||||
let totalQte = 0;
|
||||
@@ -1191,7 +1348,7 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
if (!moisFenetre.has(mm)) { horsFenetre++; return; }
|
||||
// Calibrage sur l'année en cours, seul périmètre où la master
|
||||
// measure est censée être juste.
|
||||
if (parseInt(mm.slice(0, 4), 10) === yearN && (qte > 0 || qteMaster > 0)) {
|
||||
if (avecMaster && parseInt(mm.slice(0, 4), 10) === yearN && (qte > 0 || qteMaster > 0)) {
|
||||
calibrees++;
|
||||
if (Math.abs(qte - qteMaster) <= Math.max(1, qteMaster * 0.02)) calibrOk++;
|
||||
}
|
||||
@@ -1213,7 +1370,7 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
caMag: nbMag > 0 ? ca / nbMag : 0,
|
||||
margePct: caCumule > 0 ? margeMontant / caCumule : 0,
|
||||
};
|
||||
}, timings), timings);
|
||||
}, timings, undefined, avecMaster), timings);
|
||||
|
||||
aggMonths = monthlyPayload.length;
|
||||
aggBatches++;
|
||||
@@ -1334,6 +1491,11 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
// fenêtre est extraite directement par sa propre passe, ce qui
|
||||
// donne les 5 mesures et non la seule quantité.
|
||||
void y; void qteComp;
|
||||
// Une passe ultérieure ne doit JAMAIS écraser un mois servi par
|
||||
// un zéro : hors de son année, la passe voit la combinaison
|
||||
// (article, mois) mais toutes ses mesures sont nulles.
|
||||
const precedent = monthlyByCode[code][mm];
|
||||
if (precedent && (precedent.qte ?? 0) > 0 && qte <= 0) return;
|
||||
monthlyByCode[code][mm] = full;
|
||||
};
|
||||
|
||||
@@ -1540,6 +1702,12 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
// retombe sur la sélection par codes (cf. selectionnerParFournisseur).
|
||||
parFournisseur = await selectionnerParFournisseur();
|
||||
|
||||
const attendus = monthlyPayload.map((m) => m.label);
|
||||
// Années de la fenêtre, la PLUS RÉCENTE D'ABORD — c'est l'usage de
|
||||
// l'app (« je sélectionne 2026 »), et cela met les mois les plus
|
||||
// utiles en tête des points de contrôle.
|
||||
const annees = [...new Set(attendus.map((m) => Number(m.slice(0, 4))))].sort((a, b) => b - a);
|
||||
|
||||
// ÉTAPE 1 — caler le champ `Période` du modèle sur la fenêtre.
|
||||
//
|
||||
// Toutes les mesures de l'app sont bornées par `Type_Cal`
|
||||
@@ -1548,23 +1716,23 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
// quels mois sont atteignables. Sous la période par défaut,
|
||||
// « Quantité N » ne couvre que l'année en cours : d'où les mois
|
||||
// d'août à décembre vides quelle que soit la sélection de dates.
|
||||
const couvertureMaster = await choisirPeriode();
|
||||
const couvertureMaster = await choisirPeriode(annees);
|
||||
|
||||
// ÉTAPE 2 — extraire, puis REFUSER tout résultat incomplet.
|
||||
//
|
||||
// Un cache partiel est pire que pas de cache : il se lit comme des
|
||||
// ventes nulles. Les deux chemins sont donc validés sur le même
|
||||
// critère — les 12 mois de la fenêtre, ou rien.
|
||||
const attendus = monthlyPayload.map((m) => m.label);
|
||||
const couvre12 = () => {
|
||||
const moisManquants = () => {
|
||||
const vus = new Set<string>();
|
||||
for (const parMois of Object.values(monthlyByCode)) {
|
||||
for (const [mois, v] of Object.entries(parMois)) {
|
||||
if ((v?.qte ?? 0) > 0) vus.add(mois);
|
||||
}
|
||||
}
|
||||
return attendus.every((m) => vus.has(m));
|
||||
return attendus.filter((m) => !vus.has(m));
|
||||
};
|
||||
const couvre12 = () => moisManquants().length === 0;
|
||||
|
||||
// ÉTAPE 2 — extraire ANNÉE PAR ANNÉE, comme dans l'app.
|
||||
//
|
||||
@@ -1574,26 +1742,34 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
// d'enchaîner les années couvertes par la fenêtre — ici 2025 puis
|
||||
// 2026 — pour reconstituer les 12 mois glissants.
|
||||
//
|
||||
// Ce mécanisme existait déjà (« passe N-1 ») mais restait sans
|
||||
// effet : la sélection `Période` héritée de l'ouverture de l'app
|
||||
// épinglait le contexte sur l'année en cours et annulait la
|
||||
// sélection d'année. `choisirPeriode()` l'a effacée juste avant.
|
||||
const annees = [...new Set(attendus.map((m) => Number(m.slice(0, 4))))].sort();
|
||||
diag("extraction année par année : " + JSON.stringify(annees) + " (Période libre)");
|
||||
// Chaque passe est faite SOUS la `Période` retenue : c'est elle qui
|
||||
// donne son sens à `Type_Cal='N'`. Avec `Année=2025`, « Année à
|
||||
// date » vaut l'année 2025 entière (elle est passée) ; avec
|
||||
// `Année=2026`, elle s'arrête au mois courant. L'union des deux
|
||||
// passes reconstitue donc bien les 12 mois glissants.
|
||||
diag("extraction année par année : " + JSON.stringify(annees));
|
||||
|
||||
let extraitParAnnee = false;
|
||||
if (monthDim) {
|
||||
try {
|
||||
for (const annee of annees) {
|
||||
const avant = Object.values(monthlyByCode).reduce((n, m) => n + Object.keys(m).length, 0);
|
||||
const manquantsAvant = moisManquants();
|
||||
await monthDimPath(annee);
|
||||
const apres = Object.values(monthlyByCode).reduce((n, m) => n + Object.keys(m).length, 0);
|
||||
diag(" année " + annee + " → " + (apres - avant) + " points mensuels ajoutés");
|
||||
const manquantsApres = moisManquants();
|
||||
const gagnes = manquantsAvant.filter((m) => !manquantsApres.includes(m));
|
||||
diag(
|
||||
" année " + annee + " → mois de la fenêtre désormais servis : " + JSON.stringify(gagnes) +
|
||||
" ; encore manquants : " + JSON.stringify(manquantsApres),
|
||||
);
|
||||
await pousserCheckpoint("année " + annee);
|
||||
if (manquantsApres.length === 0) break; // fenêtre complète, inutile de continuer
|
||||
}
|
||||
extraitParAnnee = couvre12();
|
||||
if (!extraitParAnnee) {
|
||||
diag("l'extraction année par année ne couvre pas les 12 mois — essai de l'agrégation directe");
|
||||
diag(
|
||||
"l'extraction année par année ne couvre pas les 12 mois (manquants : " +
|
||||
JSON.stringify(moisManquants()) + ") — essai de l'agrégation directe",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
diag("extraction année par année interrompue : " + String((e as Error)?.message || e));
|
||||
@@ -1604,8 +1780,9 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
if (!(monthDim && useExpr && (await moisParExpressions()))) {
|
||||
throw new Error(
|
||||
"extraction 12 mois refusée : ni l'extraction année par année " +
|
||||
"(Période libre, couverture master = " + couvertureMaster + "/" + attendus.length + ") " +
|
||||
"ni l'agrégation directe des faits n'ont couvert la fenêtre",
|
||||
"(couverture master annoncée = " + couvertureMaster + "/" + attendus.length + ") " +
|
||||
"ni l'agrégation directe des faits n'ont couvert la fenêtre — mois manquants : " +
|
||||
JSON.stringify(moisManquants()),
|
||||
);
|
||||
}
|
||||
await pousserCheckpoint("expressions vérifiées");
|
||||
@@ -1614,7 +1791,8 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
if (!couvre12()) {
|
||||
throw new Error(
|
||||
"extraction 12 mois refusée : la fenêtre " + JSON.stringify(attendus) +
|
||||
" n'est pas entièrement couverte — cache laissé inchangé",
|
||||
" n'est pas entièrement couverte (manquants : " + JSON.stringify(moisManquants()) +
|
||||
") — cache laissé inchangé",
|
||||
);
|
||||
}
|
||||
} else if (noDateMode) {
|
||||
|
||||
Reference in new issue
Block a user