feat(grid): make gamme filters dynamic based on available data and support Aucune state

This commit is contained in:
Michael committed 2026-02-21 19:02:44 +01:00
1 parent 86c6946c3e
commit d3b349cc31
3 files changed
+44 -18

No files matched your search

+10 -7
View File
@@ -5,32 +5,35 @@ import { cn } from "@/lib/utils";
interface GammeSelectProps {
value: GammeCode | null;
isDraft: boolean;
isDraft?: boolean;
onChange: (gamme: GammeCode) => void;
}
const GAMME_STYLES: Record<GammeCode, string> = {
const GAMME_STYLES: Record<string, string> = {
A: "border-[1px] border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/20 dark:text-emerald-400",
B: "border-[1px] border-blue-500/30 bg-blue-500/10 text-blue-700 dark:border-blue-500/30 dark:bg-blue-500/20 dark:text-blue-400",
C: "border-[1px] border-amber-500/30 bg-amber-500/10 text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/20 dark:text-amber-400",
Z: "border-[1px] border-rose-500/30 bg-rose-500/10 text-rose-700 dark:border-rose-500/30 dark:bg-rose-500/20 dark:text-rose-400",
Aucune: "border-[1px] border-slate-500/30 bg-slate-500/10 text-slate-700 dark:border-slate-500/30 dark:bg-slate-500/20 dark:text-slate-400",
};
export function GammeSelect({ value, isDraft, onChange }: GammeSelectProps) {
const displayValue = (!value || value.trim() === "") ? "Aucune" : value;
return (
<select
value={value ?? ""}
value={displayValue}
onChange={(e) => onChange(e.target.value as GammeCode)}
className={cn(
"w-full text-xs font-bold rounded-lg py-1.5 px-2 outline-none cursor-pointer shadow-sm transition-all text-center",
"focus:ring-2 focus:ring-brand-500/20",
value
? GAMME_STYLES[value]
: "border-[1px] border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-500",
GAMME_STYLES[displayValue] || "border-[1px] border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-500",
isDraft && "shadow-[0_0_8px_rgba(251,191,36,0.3)] border-amber-400/50"
)}
>
<option value="" disabled>—</option>
<option value="Aucune" disabled={displayValue !== "Aucune" && displayValue !== ""}>
{displayValue === "Aucune" || displayValue === "" ? "— (Aucune)" : "—"}
</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
@@ -30,19 +30,41 @@ export function GridFilterBar({ fournisseurs, magasins, nomenclature }: GridFilt
const router = useRouter();
const searchParams = useSearchParams();
// Compute dynamic counts per Gamme
const gammeCounts = React.useMemo(() => {
const counts: Record<string, number> = { A: 0, B: 0, C: 0, Z: 0, Total: 0 };
// Compute dynamic counts and present Gammes
const { gammeCounts, activeGammes } = React.useMemo(() => {
const counts: Record<string, number> = { Total: 0 };
const gammesPresence = new Set<string>();
rows.forEach(r => {
const effectiveGamme = draftChanges[r.codein] ?? r.codeGamme;
if (effectiveGamme && counts[effectiveGamme] !== undefined) {
counts[effectiveGamme]++;
let effectiveGamme = draftChanges[r.codein] ?? r.codeGamme;
// Handle null, empty, or undefined gammes as "Aucune"
if (!effectiveGamme || effectiveGamme.trim() === "") {
effectiveGamme = "Aucune";
}
gammesPresence.add(effectiveGamme);
counts[effectiveGamme] = (counts[effectiveGamme] || 0) + 1;
if (effectiveGamme !== "Z") {
counts.Total++; // Active references (non-Z)
}
});
return counts;
// Always ensure A, B, C, Z are available IF they have count > 0, plus any other discovered gammes
const sortedGammes = Array.from(gammesPresence).sort((a, b) => {
if (a === "Aucune") return 1; // Put "Aucune" at the end
if (b === "Aucune") return -1;
return a.localeCompare(b);
});
// Build the dynamic filter list
const filtersList: { label: string; value: string | null }[] = [
{ label: "Tous", value: null },
...sortedGammes.map(g => ({ label: g, value: g }))
];
return { gammeCounts: counts, activeGammes: filtersList };
}, [rows, draftChanges]);
const handleSupplierSelect = (code: string) => {
@@ -104,8 +126,8 @@ export function GridFilterBar({ fournisseurs, magasins, nomenclature }: GridFilt
className="flex items-center gap-1 p-0.5 rounded-lg"
style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)" }}
>
{GAMME_FILTERS.map(({ label, value }) => {
const count = value === null ? rows.length : gammeCounts[value];
{activeGammes.map(({ label, value }) => {
const count = value === null ? rows.length : gammeCounts[value] || 0;
const isActive = filters.codeGamme === value;
// Semantic colors for the badge based on Gamme
@@ -116,6 +138,7 @@ export function GridFilterBar({ fournisseurs, magasins, nomenclature }: GridFilt
case "B": return { background: "rgba(59, 130, 246, 0.15)", color: "rgb(37, 99, 235)" }; // Blue
case "C": return { background: "rgba(245, 158, 11, 0.15)", color: "rgb(217, 119, 6)" }; // Amber
case "Z": return { background: "rgba(244, 63, 94, 0.15)", color: "rgb(225, 29, 72)" }; // Rose
case "Aucune": return { background: "rgba(100, 116, 139, 0.15)", color: "rgb(71, 85, 105)" }; // Slate
default: return { background: "rgba(0,0,0,0.1)", color: "var(--text-primary)" };
}
};
+2 -2
View File
@@ -26,8 +26,8 @@ export interface VenteProduit {
margeMvt: number | null;
}
/** A/B/C/Z status codes for product categories */
export type GammeCode = "A" | "B" | "C" | "Z";
/** A/B/C/Z status codes for product categories, but can be any string from DB (like 'Aucune') */
export type GammeCode = string;
/** A product row with its 12-month sales history and computed KPIs */
export interface ProductRow {