feat: add product score explanation page detailing calculation, principles, and interpretation.

This commit is contained in:
Michael committed 2026-02-22 13:18:19 +01:00
1 parent 8767b1cf1e
commit a04d6d4e7a
2 files changed
+78 -38

No files matched your search

+21
View File
@@ -85,6 +85,27 @@ export default function ScorePage() {
</div> </div>
</InfoCard> </InfoCard>
{/* Équité & Normalisation */}
<InfoCard icon={Layers} title="Équité & Normalisation">
<div className="space-y-3">
<p className="text-[12px]" style={{ color: "var(--text-secondary)" }}>
Afin de ne pas pénaliser les produits présents dans un seul magasin, une <strong>pondération d&apos;équité</strong> est appliquée :
</p>
<div
className="rounded-[10px] px-4 py-3 flex items-center gap-4"
style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)" }}
>
<div className="text-2xl">⚖️</div>
<div className="text-[12px]" style={{ color: "var(--text-primary)" }}>
<p><strong>Produit 1 Magasin</strong> : Statistiques multipliées par <strong>2</strong></p>
<p className="mt-1" style={{ color: "var(--text-muted)" }}>
Cela permet de comparer sa performance sur une base réseau normalisée de 2 magasins.
</p>
</div>
</div>
</div>
</InfoCard>
{/* Les 3 axes */} {/* Les 3 axes */}
<InfoCard icon={Layers} title="Les 3 axes d'évaluation"> <InfoCard icon={Layers} title="Les 3 axes d'évaluation">
<div className="space-y-3"> <div className="space-y-3">
+57 -38
View File
@@ -12,7 +12,7 @@ import {
RowSelectionState, RowSelectionState,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
import { ChevronUp, ChevronDown, ChevronsUpDown, Copy, Check } from "lucide-react"; import { ChevronUp, ChevronDown, ChevronsUpDown, Copy, Check, Store } from "lucide-react";
import { useGridStore } from "@/features/grid/store/use-grid-store"; import { useGridStore } from "@/features/grid/store/use-grid-store";
import { GammeSelect } from "@/features/grid/components/gamme-select"; import { GammeSelect } from "@/features/grid/components/gamme-select";
import { HeatmapCell } from "@/features/grid/components/heatmap-cell"; import { HeatmapCell } from "@/features/grid/components/heatmap-cell";
@@ -41,25 +41,31 @@ function formatMonthLabel(key: string): string {
} }
/** /**
* Generates a deterministic color for a store name. * Returns premium styling and icons for stores.
* Harmonized with the "Silent UI" philosophy: professional, muted, and non-distracting. * Indigo for first store, Amber for second, Slate for others.
*/ */
function getStoreColor(name: string) { function getStoreConfig(name: string, index: number) {
let hash = 0; if (index === 0) {
for (let i = 0; i < name.length; i++) { return {
hash = name.charCodeAt(i) + ((hash << 5) - hash); bg: "rgba(99, 102, 241, 0.1)", // Indigo
text: "#818cf8",
border: "rgba(99, 102, 241, 0.2)",
label: "M1"
};
}
if (index === 1) {
return {
bg: "rgba(245, 158, 11, 0.1)", // Amber
text: "#fbbf24",
border: "rgba(245, 158, 11, 0.2)",
label: "M2"
};
} }
// Use a narrower, more professional range of hues (mostly Ardois/Blue/Teal/Sage)
// and lower saturation for a "muted" look.
const h = Math.abs(hash % 360);
const s = 15 + (Math.abs(hash) % 20); // Low saturation: 15-35%
const l = 40 + (Math.abs(hash) % 15); // Medium lightness: 40-55%
return { return {
bg: `hsl(${h}, ${s}%, ${l}%)`, bg: "var(--bg-elevated)",
border: `hsl(${h}, ${s}%, ${l - 10}%)`, text: "var(--text-muted)",
text: "rgba(255, 255, 255, 0.95)" border: "var(--border)",
label: name.charAt(0).toUpperCase()
}; };
} }
@@ -185,21 +191,22 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
<span className="text-[13px] font-bold truncate flex-1" title={row.original.libelle1} style={{ color: "var(--text-primary)" }}> <span className="text-[13px] font-bold truncate flex-1" title={row.original.libelle1} style={{ color: "var(--text-primary)" }}>
{row.original.libelle1} {row.original.libelle1}
</span> </span>
<div className="flex gap-1 shrink-0"> <div className="flex gap-1.5 shrink-0">
{row.original.workingStores.map((magasin) => { {row.original.workingStores.map((magasin, idx) => {
const colors = getStoreColor(magasin); const config = getStoreConfig(magasin, idx);
return ( return (
<div <div
key={magasin} key={magasin}
title={`Travaillé par : ${magasin}`} title={`Travaillé par : ${magasin} (${idx + 1 === 1 ? 'Magasin 1' : 'Magasin 2'})`}
className="w-4 h-4 rounded-full flex items-center justify-center text-[10px] font-black border shadow-sm" className="px-1.5 py-0.5 rounded-md flex items-center gap-1 text-[10px] font-black border shadow-sm transition-transform hover:scale-110"
style={{ style={{
background: colors.bg, background: config.bg,
borderColor: colors.border, borderColor: config.border,
color: colors.text, color: config.text,
}} }}
> >
{magasin.charAt(0).toUpperCase()} <Store className="w-2.5 h-2.5" strokeWidth={3} />
<span>{config.label}</span>
</div> </div>
); );
})} })}
@@ -365,15 +372,21 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
return ( return (
<div <div
ref={tableContainerRef} ref={tableContainerRef}
className="overflow-auto rounded-[12px]" className="overflow-auto rounded-[12px] scroll-smooth"
style={{ style={{
height: "calc(100vh - 240px)", height: "calc(100vh - 240px)",
background: "var(--bg-surface)", background: "var(--bg-surface)",
border: "1px solid var(--border)", border: "1px solid var(--border)",
boxShadow: "var(--shadow-sm)"
}} }}
> >
<table className="text-sm block" style={{ width: "100%", minWidth: totalWidth }}> <table className="text-sm block" style={{ width: "100%", minWidth: totalWidth }}>
<thead className="sticky top-0 z-10 block" style={{ background: "var(--bg-elevated)", borderBottom: "1px solid var(--border)" }}> <thead className="sticky top-0 z-10 block" style={{
background: "linear-gradient(to bottom, var(--bg-elevated), var(--bg-surface))",
borderBottom: "1px solid var(--border-strong)",
backdropFilter: "blur(10px)",
WebkitBackdropFilter: "blur(10px)"
}}>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className="flex w-full"> <tr key={headerGroup.id} className="flex w-full">
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
@@ -383,7 +396,7 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
return ( return (
<th <th
key={header.id} key={header.id}
className="px-2 py-2 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap select-none flex items-center" className="px-2 py-3 text-[10px] font-black uppercase tracking-[0.05em] whitespace-nowrap select-none flex items-center transition-colors hover:bg-white/5"
style={{ style={{
width: isFlexible ? "100%" : size, width: isFlexible ? "100%" : size,
flex: isFlexible ? `1 1 ${size}px` : `0 0 ${size}px`, flex: isFlexible ? `1 1 ${size}px` : `0 0 ${size}px`,
@@ -394,12 +407,15 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
}} }}
onClick={header.column.getToggleSortingHandler()} onClick={header.column.getToggleSortingHandler()}
> >
<div className={`flex items-center gap-1 cursor-pointer ${isCenter ? "justify-center w-full" : ""}`}> <div className={`flex items-center gap-1.5 cursor-pointer ${isCenter ? "justify-center w-full" : ""}`}>
{flexRender(header.column.columnDef.header, header.getContext())} {flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getCanSort() && ( {header.column.getCanSort() && (
header.column.getIsSorted() === "asc" ? <ChevronUp className="w-3 h-3" /> <div className="shrink-0 opacity-40">
: header.column.getIsSorted() === "desc" ? <ChevronDown className="w-3 h-3" /> {header.column.getIsSorted() === "asc" ? <ChevronUp className="w-3 h-3 text-emerald-500" />
: <ChevronsUpDown className="w-3 h-3 opacity-30" /> : header.column.getIsSorted() === "desc" ? <ChevronDown className="w-3 h-3 text-emerald-500" />
: <ChevronsUpDown className="w-3 h-3" />
}
</div>
)} )}
</div> </div>
</th> </th>
@@ -420,15 +436,15 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
ref={rowVirtualizer.measureElement} ref={rowVirtualizer.measureElement}
onClick={() => row.toggleSelected()} onClick={() => row.toggleSelected()}
className={cn( className={cn(
"absolute w-full flex items-center cursor-pointer transition-colors", "absolute w-full flex items-center cursor-pointer transition-all duration-200 group/row",
effectiveGamme === "Z" && "opacity-40" effectiveGamme === "Z" && "opacity-40 grayscale-[0.5] hover:grayscale-0 hover:opacity-100"
)} )}
style={{ style={{
height: `${rowHeight}px`, height: `${rowHeight}px`,
transform: `translateY(${virtualRow.start}px)`, transform: `translateY(${virtualRow.start}px)`,
borderBottom: "1px solid var(--border)", borderBottom: "1px solid var(--border)",
background: isSelected ? "var(--accent-bg)" : undefined, background: isSelected ? "var(--accent-bg)" : "transparent",
borderLeft: isSelected ? "2px solid var(--accent)" : undefined, borderLeft: isSelected ? "3px solid var(--accent)" : "3px solid transparent",
}} }}
> >
{row.getVisibleCells().map((cell) => { {row.getVisibleCells().map((cell) => {
@@ -438,7 +454,10 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
return ( return (
<td <td
key={cell.id} key={cell.id}
className={`px-2 overflow-hidden flex items-center`} className={cn(
"px-2 overflow-hidden flex items-center transition-colors group-hover/row:bg-white/5",
isSelected && "bg-transparent"
)}
style={{ style={{
width: isFlexible ? "100%" : size, width: isFlexible ? "100%" : size,
flex: isFlexible ? `1 1 ${size}px` : `0 0 ${size}px`, flex: isFlexible ? `1 1 ${size}px` : `0 0 ${size}px`,