mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-13 10:17:33 +02:00
feat: add server-side API for fetching and processing comprehensive product data for the grid.
This commit is contained in:
1 parent
99954fb001
commit
53b85381fa
7 files changed
+152
-59
No files matched your search
@@ -27,6 +27,7 @@ export function GridClient({ initialRows, codeFournisseur, nomFournisseur, fourn
|
||||
const setRows = useGridStore((s) => s.setRows);
|
||||
const setActiveGridQuery = useGridStore((s) => s.setActiveGridQuery);
|
||||
const setFilter = useGridStore((s) => s.setFilter);
|
||||
const setActiveMagasin = useGridStore((s) => s.setActiveMagasin);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [selectedCodeins, setSelectedCodeins] = useState<string[]>([]);
|
||||
@@ -61,7 +62,8 @@ export function GridClient({ initialRows, codeFournisseur, nomFournisseur, fourn
|
||||
|
||||
const scoredRows = computeProductScores([...initialRows]);
|
||||
setRows(scoredRows);
|
||||
}, [codeFournisseur, initialRows, setRows, setFilter, isMounted]);
|
||||
setActiveMagasin(magasin || "TOTAL");
|
||||
}, [codeFournisseur, initialRows, setRows, setFilter, setActiveMagasin, magasin, isMounted]);
|
||||
|
||||
const handleSave = () => {
|
||||
startTransition(async () => {
|
||||
|
||||
@@ -85,10 +85,13 @@ export function GridFilterBar({ fournisseurs, magasins }: GridFilterBarProps) {
|
||||
router.push(`/grid?${params.toString()}`);
|
||||
};
|
||||
|
||||
const { setActiveMagasin, activeMagasin } = useGridStore();
|
||||
|
||||
const handleStoreSelect = (code: string) => {
|
||||
setActiveMagasin(code);
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set("magasin", code);
|
||||
router.push(`/grid?${params.toString()}`);
|
||||
router.replace(`/grid?${params.toString()}`, { scroll: false });
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
@@ -125,7 +128,7 @@ export function GridFilterBar({ fournisseurs, magasins }: GridFilterBarProps) {
|
||||
{/* Store Selector */}
|
||||
<StoreCombobox
|
||||
magasins={magasins}
|
||||
selectedCode={searchParams.get("magasin")}
|
||||
selectedCode={activeMagasin === "TOTAL" ? null : activeMagasin}
|
||||
onSelect={handleStoreSelect}
|
||||
className="w-[220px]"
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react";
|
||||
|
||||
interface HeatmapCellProps {
|
||||
value: number | null;
|
||||
tooltipStock?: number | null;
|
||||
}
|
||||
|
||||
function getHeatmapClass(value: number | null): string {
|
||||
@@ -13,14 +14,20 @@ function getHeatmapClass(value: number | null): string {
|
||||
return "heatmap-zero";
|
||||
}
|
||||
|
||||
export const HeatmapCell = React.memo(({ value }: HeatmapCellProps) => {
|
||||
export const HeatmapCell = React.memo(({ value, tooltipStock }: HeatmapCellProps) => {
|
||||
const heatClass = getHeatmapClass(value);
|
||||
const display = value === null || value === 0 ? "0" : Math.round(Math.abs(value)).toString();
|
||||
|
||||
const titleParts: string[] = [];
|
||||
if (value !== null && value !== 0) titleParts.push(`${Math.abs(value).toLocaleString("fr-FR")} unités`);
|
||||
else titleParts.push("Aucune vente");
|
||||
if (tooltipStock != null) titleParts.push(`Stock fin de mois : ${tooltipStock.toLocaleString("fr-FR")}`);
|
||||
const title = titleParts.join(" · ");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded text-center text-[12px] tabular-nums px-0.5 py-0.5 font-bold tracking-tighter transition-colors min-h-[24px] flex items-center justify-center font-mono-nums ${heatClass}`}
|
||||
title={value !== null ? `${Math.abs(value).toLocaleString("fr-FR")} unités` : "Aucune vente"}
|
||||
title={title}
|
||||
>
|
||||
{display}
|
||||
</div>
|
||||
|
||||
@@ -184,7 +184,7 @@ GridRow.displayName = "GridRow";
|
||||
|
||||
export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
|
||||
// L'abonnement doit être minimal ici ! PAS de draftChanges ni de setDraftGamme.
|
||||
const { rows, filters, displayDensity, draftChanges } = useGridStore();
|
||||
const { rows, filters, displayDensity, draftChanges, activeMagasin } = useGridStore();
|
||||
|
||||
// Filtre client-side par code3 (famille) et codeGamme
|
||||
const filteredData = useMemo(() => {
|
||||
@@ -456,50 +456,75 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
|
||||
header: () => <div className="text-center w-full">{formatMonthLabel(monthKey)}</div>,
|
||||
size: 68,
|
||||
enableSorting: false,
|
||||
cell: ({ row }: { row: { original: ProductRow } }) => (
|
||||
<HeatmapCell value={row.original.sales12m[monthKey] ?? null} />
|
||||
),
|
||||
cell: ({ row }: { row: { original: ProductRow } }) => {
|
||||
const qty = activeMagasin === "TOTAL"
|
||||
? (row.original.sales12m[monthKey] ?? null)
|
||||
: (row.original.sales12mByStore?.[activeMagasin]?.[monthKey] ?? null);
|
||||
const stock = activeMagasin === "TOTAL"
|
||||
? (row.original.stock12m[monthKey] ?? null)
|
||||
: (row.original.stock12mByStore?.[activeMagasin]?.[monthKey] ?? null);
|
||||
return <HeatmapCell value={qty} tooltipStock={stock} />;
|
||||
},
|
||||
})),
|
||||
{
|
||||
accessorKey: "totalQuantite",
|
||||
id: "totalQuantite",
|
||||
header: () => <div className="text-center w-full">Tot. 12m</div>,
|
||||
size: 90,
|
||||
cell: ({ getValue }) => (
|
||||
<div className="mx-auto w-[60px] text-center px-1 tabular-nums text-sm font-black py-1.5 rounded-md border" style={{
|
||||
background: "var(--bg-elevated)",
|
||||
color: "var(--text-primary)",
|
||||
borderColor: "var(--border-strong)"
|
||||
}}>
|
||||
{Math.round(getValue<number>()).toLocaleString("fr-FR")}
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: { original: ProductRow } }) => {
|
||||
const qty = activeMagasin === "TOTAL"
|
||||
? row.original.totalQuantite
|
||||
: (row.original.quantiteByStore?.[activeMagasin] ?? 0);
|
||||
return (
|
||||
<div className="mx-auto w-[60px] text-center px-1 tabular-nums text-sm font-black py-1.5 rounded-md border" style={{
|
||||
background: "var(--bg-elevated)",
|
||||
color: "var(--text-primary)",
|
||||
borderColor: "var(--border-strong)"
|
||||
}}>
|
||||
{Math.round(qty).toLocaleString("fr-FR")}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "totalCa",
|
||||
id: "totalCa",
|
||||
header: () => <div className="text-center w-full">CA</div>,
|
||||
size: 90,
|
||||
cell: ({ getValue }) => (
|
||||
<div className="text-center tabular-nums text-[13px] font-bold" style={{ color: "var(--text-primary)" }}>
|
||||
{Math.round(getValue<number>()).toLocaleString("fr-FR")} €
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: { original: ProductRow } }) => {
|
||||
const ca = activeMagasin === "TOTAL"
|
||||
? row.original.totalCa
|
||||
: (row.original.caByStore?.[activeMagasin] ?? 0);
|
||||
return (
|
||||
<div className="text-center tabular-nums text-[13px] font-bold" style={{ color: "var(--text-primary)" }}>
|
||||
{Math.round(ca).toLocaleString("fr-FR")} €
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "totalMarge",
|
||||
id: "totalMarge",
|
||||
header: () => <div className="text-center w-full">Marge</div>,
|
||||
size: 110,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="tabular-nums text-[13px] font-bold" style={{ color: "var(--text-primary)" }}>
|
||||
{Math.round(row.original.totalMarge).toLocaleString("fr-FR")} €
|
||||
</span>
|
||||
<span className="tabular-nums text-[10px] font-bold opacity-70" style={{
|
||||
color: row.original.tauxMarge >= 40 ? "var(--accent-success)" : row.original.tauxMarge >= 25 ? "var(--accent-warning)" : "var(--accent-error)"
|
||||
}}>
|
||||
{row.original.tauxMarge.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: { original: ProductRow } }) => {
|
||||
const marge = activeMagasin === "TOTAL"
|
||||
? row.original.totalMarge
|
||||
: (row.original.margeByStore?.[activeMagasin] ?? 0);
|
||||
const ca = activeMagasin === "TOTAL"
|
||||
? row.original.totalCa
|
||||
: (row.original.caByStore?.[activeMagasin] ?? 0);
|
||||
const taux = ca > 0 ? (marge / ca) * 100 : 0;
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="tabular-nums text-[13px] font-bold" style={{ color: "var(--text-primary)" }}>
|
||||
{Math.round(marge).toLocaleString("fr-FR")} €
|
||||
</span>
|
||||
<span className="tabular-nums text-[10px] font-bold opacity-70" style={{
|
||||
color: taux >= 40 ? "var(--accent-success)" : taux >= 25 ? "var(--accent-warning)" : "var(--accent-error)"
|
||||
}}>
|
||||
{taux.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "gammeInitial",
|
||||
@@ -528,7 +553,7 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
], [MONTHS_12]); // Dépendances extrêmement stables : pas de re-render du header !
|
||||
], [MONTHS_12, activeMagasin]); // activeMagasin déclenche re-render des cellules mensuelles et totaux
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredData,
|
||||
|
||||
Reference in new issue
Block a user