feat: Implement product heatmap grid with comprehensive data aggregation and PostgreSQL client for product rows.

This commit is contained in:
Michael committed 2026-03-23 16:09:09 +01:00
1 parent bba6fe05e8
commit ffa257fda1
5 files changed
+73 -37

No files matched your search

+23 -13
View File
@@ -125,10 +125,11 @@ export async function getProductRows(input: GetProductRowsInput): Promise<Produc
const siteByPeriod = bySite.get(row.site)!;
if (!siteByPeriod.has(periode)) siteByPeriod.set(periode, { qty: 0, ca: 0, marge: 0, stock: 0, receptions: 0 });
const sp = siteByPeriod.get(periode)!;
sp.qty += Number(row.qte_vendue) || 0;
sp.ca += Number(row.ca_ht) || 0;
sp.marge += Number(row.marge) || 0;
sp.stock = Math.max(sp.stock, Number(row.stock_fin_mois) || 0);
sp.qty += Number(row.qte_vendue) || 0;
sp.ca += Number(row.ca_ht) || 0;
sp.marge += Number(row.marge) || 0;
sp.stock = Number(row.stock_fin_mois) || 0; // SQL donne déjà le dernier mouvement
sp.receptions += Number(row.qte_recue) || 0;
// Suivi magasins actifs (pour workingStores) : 1 vente suffit
if (Number(row.qte_vendue) > 0) {
@@ -165,19 +166,28 @@ export async function getProductRows(input: GetProductRowsInput): Promise<Produc
// Breakdowns par site pour le switch client-side
const siteMap = mensuelBySite.get(codein);
if (siteMap) {
product.sales12mByStore = {};
product.stock12mByStore = {};
product.caByStore = {};
product.quantiteByStore = {};
product.margeByStore = {};
product.sales12mByStore = {};
product.stock12mByStore = {};
product.receptions12mByStore = {};
product.caByStore = {};
product.quantiteByStore = {};
product.margeByStore = {};
for (const [site, siteByPeriod] of siteMap.entries()) {
product.sales12mByStore[site] = {};
product.stock12mByStore[site] = {};
product.sales12mByStore[site] = {};
product.stock12mByStore[site] = {};
product.receptions12mByStore![site] = {};
let sQty = 0, sCa = 0, sMarge = 0;
let lastSiteStock = 0;
for (const periode of sortedPeriods) {
const sp = siteByPeriod.get(periode);
product.sales12mByStore[site][periode] = sp?.qty ?? 0;
product.stock12mByStore[site][periode] = sp?.stock ?? 0;
product.sales12mByStore[site][periode] = sp?.qty ?? 0;
product.receptions12mByStore![site][periode] = sp?.receptions ?? 0;
if (sp) {
product.stock12mByStore[site][periode] = sp.stock;
lastSiteStock = sp.stock;
} else {
product.stock12mByStore[site][periode] = lastSiteStock; // carry-forward
}
sQty += sp?.qty ?? 0;
sCa += sp?.ca ?? 0;
sMarge += sp?.marge ?? 0;
@@ -3,6 +3,7 @@ import React from "react";
interface HeatmapCellProps {
value: number | null;
tooltipStock?: number | null;
tooltipReceptions?: number | null;
}
function getHeatmapClass(value: number | null): string {
@@ -14,15 +15,16 @@ function getHeatmapClass(value: number | null): string {
return "heatmap-zero";
}
export const HeatmapCell = React.memo(({ value, tooltipStock }: HeatmapCellProps) => {
export const HeatmapCell = React.memo(({ value, tooltipStock, tooltipReceptions }: 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(" · ");
if (tooltipReceptions != null && tooltipReceptions > 0)
titleParts.push(`Entrées : +${Math.round(tooltipReceptions).toLocaleString("fr-FR")}`);
if (tooltipStock != null)
titleParts.push(`Stock fin de mois : ${Math.round(tooltipStock).toLocaleString("fr-FR")}`);
const title = titleParts.length > 0 ? titleParts.join(" · ") : (value ? `${Math.abs(value).toLocaleString("fr-FR")} unités` : "Aucune vente");
return (
<div
@@ -463,7 +463,10 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
const stock = activeMagasin === "TOTAL"
? (row.original.stock12m[monthKey] ?? null)
: (row.original.stock12mByStore?.[activeMagasin]?.[monthKey] ?? null);
return <HeatmapCell value={qty} tooltipStock={stock} />;
const receptions = activeMagasin === "TOTAL"
? (row.original.receptions12m[monthKey] ?? null)
: (row.original.receptions12mByStore?.[activeMagasin]?.[monthKey] ?? null);
return <HeatmapCell value={qty} tooltipStock={stock} tooltipReceptions={receptions} />;
},
})),
{
@@ -571,7 +574,7 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
enableRowSelection: true,
globalFilterFn: (row, columnId, filterValue) => {
globalFilterFn: (row, _columnId, filterValue) => {
const search = String(filterValue).toLowerCase();
const libelle = String(row.original.libelle1 || "").toLowerCase();
const code = String(row.original.codein || "").toLowerCase();