Grille : détail mensuel au clic sur le total, bloc mensuel repliable

La colonne « Tot. 12m » dit combien, jamais comment. Un clic sur la case
ouvre désormais le détail des 12 mois glissants, empilé dans l'ordre où on
le lit : les ventes mensuelles, puis les entrées en stock, puis le stock de
fin de mois magasin par magasin — un stock de 40 réparti sur deux sites et
un stock de 40 bloqué sur un seul ne se pilotent pas pareil.

Les deux premières séries suivent le magasin actif de la Grille, par
cohérence avec la case cliquée ; la ventilation par magasin reste entière,
c'est son objet. Les barres se dessinent de part et d'autre d'un zéro
explicite : une quantité mensuelle peut être négative (retours supérieurs
aux ventes) et sortirait sinon du cadre par le bas.

Le bloc des douze colonnes mensuelles se replie d'un bouton. L'état est un
drapeau à part, et non douze entrées de `columnVisibility` : masquer douze
colonnes une par une n'est pas une manipulation, et les identifiants
`month_YYYYMM` changent à chaque nouveau mois alors que le drapeau, lui,
survit à la fenêtre glissante. Les colonnes sont retirées du modèle plutôt
que cachées — inutile de faire calculer douze cellules par ligne à TanStack
pour ne rien peindre.

`TuileStat` sort de heatmap-grid.tsx vers un module partagé, la modale de
tendance réseau et la nouvelle modale s'en servant toutes deux.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR
This commit is contained in:
Claude committed 2026-08-13 10:54:24 +00:00
1 parent 92769535dc
commit 2ae523a804
4 files changed
+468 -42

No files matched your search

+19 -1
View File
@@ -20,6 +20,16 @@ interface GridState {
columnVisibility: Record<string, boolean>;
/** Persisted column sizing state */
columnSizing: Record<string, number>;
/**
* Bloc des 12 colonnes mensuelles affiché ou non dans la Grille.
*
* Séparé de `columnVisibility` : masquer douze colonnes une par une n'est pas
* une manipulation, et l'état « bloc replié » doit survivre au renouvellement
* de la fenêtre glissante — les identifiants `month_YYYYMM` changent chaque
* mois, un drapeau ne change pas.
*/
showMonthlySales: boolean;
setShowMonthlySales: (visible: boolean) => void;
/** Currently displayed store: "TOTAL" | "292" | "579" */
activeMagasin: string;
@@ -75,7 +85,7 @@ function computeSummary(rows: ProductRow[], drafts: Record<string, GammeCode>, m
/** Sous-ensemble réellement écrit dans le navigateur (cf. `partialize`). */
type GridPersisted = Pick<
GridState,
"filters" | "displayDensity" | "draftChanges" | "activeGridQuery" | "columnVisibility" | "columnSizing"
"filters" | "displayDensity" | "draftChanges" | "activeGridQuery" | "columnVisibility" | "columnSizing" | "showMonthlySales"
>;
export const useGridStore = create<GridState>()(
@@ -106,6 +116,9 @@ export const useGridStore = create<GridState>()(
activeGridQuery: "",
columnVisibility: {},
columnSizing: {},
showMonthlySales: true,
setShowMonthlySales: (visible) => set({ showMonthlySales: visible }),
setActiveMagasin: (code) => {
set({ activeMagasin: code });
@@ -239,6 +252,10 @@ export const useGridStore = create<GridState>()(
const c = etat.filters.code3;
etat.filters.code3 = typeof c === "string" && c !== "" ? [c] : Array.isArray(c) ? c : null;
}
// Drapeau ajouté après coup : un état persisté antérieur n'en a
// pas, et une valeur `undefined` écraserait la valeur par défaut
// à la fusion (le bloc mensuel disparaîtrait sans raison).
if (typeof etat.showMonthlySales !== "boolean") etat.showMonthlySales = true;
return etat;
},
// Only persist filters, display density, drafts, active grid query, and column visibility/sizing
@@ -249,6 +266,7 @@ export const useGridStore = create<GridState>()(
activeGridQuery: state.activeGridQuery,
columnVisibility: state.columnVisibility,
columnSizing: state.columnSizing,
showMonthlySales: state.showMonthlySales,
}),
}
)