diff --git a/src/features/ai-copilot/business/analysis-engine.ts b/src/features/ai-copilot/business/analysis-engine.ts index 4bd1343..340fa64 100644 --- a/src/features/ai-copilot/business/analysis-engine.ts +++ b/src/features/ai-copilot/business/analysis-engine.ts @@ -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 ?`; } diff --git a/src/features/ai-copilot/models/ai-analysis.types.ts b/src/features/ai-copilot/models/ai-analysis.types.ts index f997fee..6139a39 100644 --- a/src/features/ai-copilot/models/ai-analysis.types.ts +++ b/src/features/ai-copilot/models/ai-analysis.types.ts @@ -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; codeGamme: string | null; diff --git a/src/features/grid/components/bulk-ai-analyzer.tsx b/src/features/grid/components/bulk-ai-analyzer.tsx index af8b3c7..a1b4f52 100644 --- a/src/features/grid/components/bulk-ai-analyzer.tsx +++ b/src/features/grid/components/bulk-ai-analyzer.tsx @@ -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(); + 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(); + 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])