merge: piloter Période + garde-fou d'intégrité + sélection par dimension Mois

Fusion de deux lignes de travail parallèles sur main, complémentaires et non
concurrentes :

  cd2e77e  selectionnerFenetreViaDimensionMois() — sélectionne les 12 mois par
           qElemNumber sur la dimension maître Mois. C'est ce qui marche : les
           champs Date exposés sont dissociés des faits (100 % du total avant
           comme après), la dimension maître porte les vraies valeurs.
  4ef0e9f  garde-fou d'intégrité — refuse d'écrire le cache si la fenêtre n'est
           pas entièrement couverte. C'est pourquoi le cache est resté inchangé
           au lieu d'être corrompu.
  726fb84  choisirPeriode() — le maillon manquant.

Le dump des expressions donne la cause de fond :

  « Quantité N »    = Sum({<Type_Cal={'N'}>} quantite)
  « Quantité COMP » = Sum({<Type_Cal={'$(vPeriod_comp)'}…>} quantite)

Un pont de périodes duplique les lignes de faits ; Type_Cal marque la période
analysée (N) ou de comparaison (COMP), et le champ Période (4 valeurs) décide
de quelle période il s'agit. Sélectionner correctement les mois ne suffit donc
pas : sous la période par défaut, « Quantité N » rend 0 sur tout mois hors
période courante, même parfaitement sélectionné.

Orchestration fusionnée :
  1. choisirPeriode() essaie chaque valeur de Période et MESURE, sur un cube
     [Mois] × Quantité N, combien de mois de la fenêtre elle rend disponibles.
     Aucun libellé n'est deviné.
  2. Si une période couvre les 12 mois → master measures (exactes sur toute la
     fenêtre). Sinon → agrégation directe des faits, en secours.
  3. Dans les deux cas, la couverture des 12 mois est revérifiée avant
     persistance ; sinon l'extraction est refusée et le cache laissé intact.

La tendance conserve la règle stricte de 4ef0e9f (les 12 clés explicitement
présentes, ou pas de tendance), qui écarte aussi le « +4100 % » calculé sur deux
points pour un article vendu depuis mai.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
This commit is contained in:
Claude committed 2026-07-31 11:27:51 +00:00
commit 5f3760f03e
8 files changed
+389 -145

No files matched your search

+15 -16
View File
@@ -60,14 +60,15 @@ export const TREND_COLOR: Record<NetworkTrend["direction"], string> = {
* présent dans le cache est donc **ignoré** — sinon la pente est faussée par un
* mois tronqué.
*
* Un mois **absent** du cache n'est PAS supposé nul : l'extraction ne couvre pas
* toujours les 12 mois (les mesures Qlik « N » ne savent pas remonter au-delà de
* l'année en cours). La sync écrit un zéro **explicite** sur chaque mois qu'elle
* a réellement couvert sans vente ; la tendance ne retient donc que les mois
* présents comme clés, et ignore purement et simplement les autres.
* Une série n'est affichée que si les **12 clés sont explicitement présentes**.
* L'extracteur Qlik écrit lui-même les mois sans faits à 0 après avoir validé
* la fenêtre et l'égalité avec le total Qlik. Une ancienne extraction partielle
* ne peut donc plus être présentée comme une tendance réelle.
*
* Sans cette distinction, un article vendu seulement depuis mai voyait sa
* tendance calculée sur deux points — « +4 100 % · Forte hausse ».
* Cette exigence du « tout ou rien » évite deux écueils observés en production :
* une tendance calculée sur deux points (« +4 100 % · Forte hausse » pour un
* article vendu depuis mai), et des mois non extraits comptés comme des mois
* sans vente.
*
* Utilise tous les points (robuste au bruit d'un mois isolé). L'indicateur `pct` est la
* variation modélisée sur la période (pente × durée) rapportée à la moyenne.
@@ -79,16 +80,14 @@ export function computeNetworkTrend(
): NetworkTrend {
const empty: NetworkTrend = { values: [], labels: [], direction: "flat", pct: null, hasData: false };
if (!qteByMonth) return empty;
const presents = Object.keys(qteByMonth).sort(); // "YYYY-MM" trie chronologiquement
if (presents.length === 0) return empty;
const labels = buildRolling12QlikMonths(now);
const complet = labels.every((label) =>
Object.prototype.hasOwnProperty.call(qteByMonth, label) &&
Number.isFinite(Number(qteByMonth[label])),
);
if (!complet) return empty;
const fenetre = buildRolling12QlikMonths(now);
// Seuls les mois RÉELLEMENT extraits comptent : ceux que la sync n'a pas
// couverts sont absents des clés et sont écartés, plutôt que comptés à zéro.
const labels = fenetre.filter((m) => qteByMonth[m] != null);
if (labels.length === 0) return empty;
const values = labels.map((l) => Number(qteByMonth[l]) || 0);
const values = labels.map((l) => Number(qteByMonth[l]));
const n = values.length;
let pct: number | null = null;
let direction: NetworkTrend["direction"] = "flat";