mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat(grid): add store participation icons in designation column
This commit is contained in:
1 parent
29936cf3e1
commit
cb7cbf873f
3 files changed
+55
-11
No files matched your search
@@ -34,28 +34,36 @@ export async function getProductRows(input: GetProductRowsInput): Promise<Produc
|
||||
)
|
||||
);
|
||||
|
||||
// Group by codein and aggregate monthly sales
|
||||
// Group by codein and aggregate monthly sales + track store participation
|
||||
const productMap = new Map<string, ProductRow>();
|
||||
const storeParticipationMap = new Map<string, Map<string, Set<string>>>(); // codein -> magasin -> Set of periods
|
||||
|
||||
for (const row of rows) {
|
||||
// Ignorer les données qui ne font pas partie des 12 mois complets
|
||||
if (!allowedPeriods.has(row.periode)) continue;
|
||||
|
||||
const existing = productMap.get(row.codein);
|
||||
const monthKey = row.periode;
|
||||
const { codein, magasin, periode } = row;
|
||||
|
||||
// Suivi de la participation par magasin
|
||||
if (!storeParticipationMap.has(codein)) storeParticipationMap.set(codein, new Map());
|
||||
const productStores = storeParticipationMap.get(codein)!;
|
||||
if (!productStores.has(magasin)) productStores.set(magasin, new Set());
|
||||
productStores.get(magasin)!.add(periode);
|
||||
|
||||
const existing = productMap.get(codein);
|
||||
const qty = Math.abs(parseFloat(row.quantite?.toString() ?? "0"));
|
||||
const ca = Math.abs(parseFloat(row.montantMvt?.toString() ?? "0"));
|
||||
const marge = Math.abs(parseFloat(row.margeMvt?.toString() ?? "0"));
|
||||
|
||||
if (existing) {
|
||||
existing.sales12m[monthKey] = (existing.sales12m[monthKey] ?? 0) + qty;
|
||||
existing.sales12m[periode] = (existing.sales12m[periode] ?? 0) + qty;
|
||||
existing.totalQuantite += qty;
|
||||
existing.totalCa += ca;
|
||||
existing.totalMarge += marge;
|
||||
existing.tauxMarge = existing.totalCa > 0 ? (existing.totalMarge / existing.totalCa) * 100 : 0;
|
||||
} else {
|
||||
productMap.set(row.codein, {
|
||||
codein: row.codein,
|
||||
productMap.set(codein, {
|
||||
codein,
|
||||
codeFournisseur: row.codeFournisseur ?? "",
|
||||
nomFournisseur: row.nomFournisseur ?? "",
|
||||
libelle1: row.libelle1 ?? "",
|
||||
@@ -65,16 +73,32 @@ export async function getProductRows(input: GetProductRowsInput): Promise<Produc
|
||||
libelle3: row.libelle3 ?? "",
|
||||
codeGamme: (row.codeGamme as GammeCode | null),
|
||||
codeGammeDraft: null,
|
||||
sales12m: { [monthKey]: qty },
|
||||
sales12m: { [periode]: qty },
|
||||
totalQuantite: qty,
|
||||
totalCa: ca,
|
||||
totalMarge: marge,
|
||||
tauxMarge: ca > 0 ? (marge / ca) * 100 : 0,
|
||||
score: 0,
|
||||
workingStores: [],
|
||||
aiRecommendation: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Finaliser la liste des magasins "actifs" (travaillant le produit : >= 3 mois de vente)
|
||||
for (const [codein, product] of productMap.entries()) {
|
||||
const productStores = storeParticipationMap.get(codein);
|
||||
if (productStores) {
|
||||
const workingStores: string[] = [];
|
||||
for (const [magasin, periods] of productStores.entries()) {
|
||||
if (magasin === "TOTAL") continue;
|
||||
if (periods.size >= 3) {
|
||||
workingStores.push(magasin);
|
||||
}
|
||||
}
|
||||
product.workingStores = workingStores.sort();
|
||||
}
|
||||
}
|
||||
|
||||
return computeProductScores(Array.from(productMap.values()));
|
||||
}
|
||||
@@ -127,10 +127,29 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
|
||||
accessorKey: "libelle1",
|
||||
header: "Désignation",
|
||||
size: 280,
|
||||
cell: ({ getValue }) => (
|
||||
<span className="text-[13px] font-bold truncate block max-w-[270px]" title={getValue<string>()} style={{ color: "var(--text-primary)" }}>
|
||||
{getValue<string>()}
|
||||
</span>
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2 overflow-hidden w-full pr-1">
|
||||
<span className="text-[13px] font-bold truncate flex-1" title={row.original.libelle1} style={{ color: "var(--text-primary)" }}>
|
||||
{row.original.libelle1}
|
||||
</span>
|
||||
<div className="flex gap-1 shrink-0">
|
||||
{row.original.workingStores.map((magasin) => (
|
||||
<div
|
||||
key={magasin}
|
||||
title={`Travaillé par : ${magasin}`}
|
||||
className="w-4 h-4 rounded-full flex items-center justify-center text-[10px] font-black border"
|
||||
style={{
|
||||
background: "var(--bg-elevated)",
|
||||
borderColor: "var(--border-strong)",
|
||||
color: "var(--text-secondary)",
|
||||
boxShadow: "var(--shadow-xs)"
|
||||
}}
|
||||
>
|
||||
{magasin.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface ProductRow {
|
||||
tauxMarge: number;
|
||||
/** Combined performance score (0-10) */
|
||||
score: number;
|
||||
workingStores: string[];
|
||||
aiRecommendation?: string | null;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user