feat: introduce AI-powered bulk product analysis feature with dedicated engine, types, and UI component.

This commit is contained in:
Michael committed 2026-02-22 12:32:09 +01:00
1 parent 335cf15ffa
commit 8b9803fffc
3 files changed
+44 -13

No files matched your search

@@ -10,11 +10,11 @@ En analysant les données de ventes fournies, génère une recommandation de gam
- [Z] - SORTIE : Produit en fin de cycle de vie, sans rotation significative ou en chute libre, devant être retiré du référencement.
Critères de pondération et Règles Métier Strictes :
1. Normalisation Totale (Base 2 magasins) : TOUTES les statistiques fournies (Totaux et Historique Mensuel) sont PONDÉRÉES par 2 si le produit n'est présent que dans 1 magasin. L'objectif est de simuler une performance sur une base réseau de 2 magasins.
2. Seuil "Permanent" (A) : Pour être A, un produit doit avoir un volume PONDÉRÉ significatif (ex: > 30-50 unités/an).
3. Seuil "Sortie" (Z) : Un produit avec moins de 10 unités pondérées par an et un CA faible est un candidat naturel à la sortie (Z).
4. Volume/CA/Marge : Un produit "A" doit justifier sa place en rayon par son flux (volume) OU sa rentabilité brute (CA/Marge). S'il est faible sur les deux, c'est un Z.
5. Poids Relatif : Analyse la performance par rapport aux moyennes fournies.
1. Normalisation Totale (Base 2 magasins) : TOUTES les statistiques fournies sont PONDÉRÉES par 2 si le produit n'est au catalogue que de 1 magasin.
2. Segmentation par Rayon (Univers) : Privilégie la comparaison "Intra-Rayon". Un produit doit être performant par rapport aux standards de son propre Rayon (Niveau 2).
3. Seuil "Permanent" (A) : Pour être A, un produit doit avoir un volume PONDÉRÉ significatif (ex: > 30-50 unités/an).
4. Seuil "Sortie" (Z) : Un produit avec moins de 10 unités pondérées par an et un CA faible est un candidat à la sortie (Z).
5. Équilibre : Un produit "A" doit justifier sa place par son flux OU sa rentabilité brute.
Ta réponse doit être courte, directe et sans complaisance.
Format impératif : "[Recommandation]: [Justification courte]"`;
@@ -28,18 +28,19 @@ Format impératif : "[Recommandation]: [Justification courte]"`;
const volumeInfo = `Stats Réelles : ${Math.round(p.totalQuantite)}u (${p.totalCa.toFixed(2)}€) sur ${p.storeCount} mag.
Stats Pondérées (Base 2 mag) : ${Math.round(p.weightedTotalQuantite || 0)}u (${(p.weightedTotalCa || 0).toFixed(2)}€)`;
const benchmarks = p.avgQtyGroup1 !== undefined && p.avgQtyGroup2 !== undefined
? `\nBenchmarks (Volumes moyens) :
- Moyenne mag unique : ${Math.round(p.avgQtyGroup1)}u
- Moyenne multi-magasins : ${Math.round(p.avgQtyGroup2)}u`
: "";
const benchmarks = `
Benchmarks Fournisseur (Global) :
- Moyenne 1 mag: ${Math.round(p.avgQtyGroup1 || 0)}u | Multi-mag: ${Math.round(p.avgQtyGroup2 || 0)}u
Benchmarks Rayon ("${p.libelleNiveau2}") :
- Moyenne 1 mag: ${Math.round(p.avgQtyRayon1 || 0)}u | Multi-mag: ${Math.round(p.avgQtyRayon2 || 0)}u`;
return `Produit : "${p.libelle1}" (Ref: ${p.codein})
Rayon : ${p.libelleNiveau2 || "Non classé"}
Gamme actuelle : ${p.codeGamme ?? "Non définie"}
${volumeInfo}${benchmarks}
Indicateurs Clés :
- Marge : ${p.tauxMarge.toFixed(1)}%
- Historique mensuel : ${monthlySummary}
- Historique mensuel (Pondéré) : ${monthlySummary}
Quelle est ta recommandation (A, C ou Z) et pourquoi ?`;
}
@@ -1,14 +1,17 @@
export interface ProductAnalysisInput {
codein: string;
libelle1: string;
libelleNiveau2?: string;
totalCa: number;
tauxMarge: number;
totalQuantite: number;
weightedTotalQuantite?: number;
weightedTotalCa?: number;
avgTotalQuantite?: number;
avgQtyGroup1?: number; // Moyenne volume pour produits à 1 magasin
avgQtyGroup2?: number; // Moyenne volume pour produits à 2 magasins+
avgQtyGroup1?: number; // Moyenne volume pour produits à 1 magasin (Fournisseur)
avgQtyGroup2?: number; // Moyenne volume pour produits à 2 magasins+ (Fournisseur)
avgQtyRayon1?: number; // Moyenne volume pour produits à 1 magasin (Rayon)
avgQtyRayon2?: number; // Moyenne volume pour produits à 2 magasins+ (Rayon)
storeCount: number;
sales12m: Record<string, number>;
codeGamme: string | null;
@@ -33,13 +33,38 @@ export function BulkAiAnalyzer() {
const avgQty1 = rowsGroup1.length > 0 ? rowsGroup1.reduce((s: number, r: any) => s + (r.totalQuantite || 0), 0) / rowsGroup1.length : 0;
const avgQty2 = rowsGroup2.length > 0 ? rowsGroup2.reduce((s: number, r: any) => s + (r.totalQuantite || 0), 0) / rowsGroup2.length : 0;
// Benchmarks par Rayon (Nomenclature Niveau 2)
const rayonStats = new Map<string, { qty1: number[], qty2: number[] }>();
rows.forEach((r: any) => {
const rayon = r.libelleNiveau2 || "Général";
if (!rayonStats.has(rayon)) rayonStats.set(rayon, { qty1: [], qty2: [] });
const stats = rayonStats.get(rayon)!;
const sc = r.workingStores?.length || 1;
if (sc === 1) stats.qty1.push(r.totalQuantite || 0);
else stats.qty2.push(r.totalQuantite || 0);
});
const rayonBenchmarks = new Map<string, { avg1: number, avg2: number }>();
rayonStats.forEach((stats, rayon) => {
const totalCount = stats.qty1.length + stats.qty2.length;
// Seuil de 3 produits pour justifier un benchmark spécifique par rayon
if (totalCount >= 3) {
rayonBenchmarks.set(rayon, {
avg1: stats.qty1.length > 0 ? stats.qty1.reduce((a, b) => a + b, 0) / stats.qty1.length : 0,
avg2: stats.qty2.length > 0 ? stats.qty2.reduce((a, b) => a + b, 0) / stats.qty2.length : 0
});
}
});
const productPayloads: ProductAnalysisInput[] = rows.map(r => {
const sc = r.workingStores?.length || 1;
const weight = sc === 1 ? 2 : 1; // On ramène tout sur une base 2 magasins
const rb = rayonBenchmarks.get(r.libelleNiveau2 || "Général");
return {
codein: r.codein,
libelle1: r.libelle1 || "",
libelleNiveau2: r.libelleNiveau2 || "Général",
totalCa: r.totalCa || 0,
tauxMarge: r.tauxMarge || 0,
totalQuantite: r.totalQuantite || 0,
@@ -48,6 +73,8 @@ export function BulkAiAnalyzer() {
avgTotalQuantite: avgQty,
avgQtyGroup1: avgQty1,
avgQtyGroup2: avgQty2,
avgQtyRayon1: rb ? rb.avg1 : avgQty1,
avgQtyRayon2: rb ? rb.avg2 : avgQty2,
storeCount: sc,
sales12m: Object.fromEntries(
Object.entries(r.sales12m || {}).map(([month, val]) => [month, val * weight])