diff --git a/src/features/ai-copilot/business/analysis-engine.ts b/src/features/ai-copilot/business/analysis-engine.ts index ba43728..56edb01 100644 --- a/src/features/ai-copilot/business/analysis-engine.ts +++ b/src/features/ai-copilot/business/analysis-engine.ts @@ -7,15 +7,19 @@ export class AnalysisEngine { RÈGLES D'OR DE DÉCISION : 1. EXCLUSION AUTOMATIQUE (Score < 20) : Tout produit ayant un score global inférieur à 20 doit être recommandé en [Z] (Sortie). C'est une règle de sécurité non négociable. 2. ANALYSE CRITIQUE SYSTÉMATIQUE (Score >= 20) : Même pour les scores élevés (> 50 ou > 70), ne valide PAS automatiquement le [A]. Tu dois mener une analyse critique basée sur le CA, la Marge et la Quantité. -3. VALIDATION PAR LA VALEUR : Pour recommander [A], le produit doit prouver sa valeur réelle (ex: Marge élevée, CA significatif, ou Régularité de service irréprochable). Si les volumes sont dérisoires et la marge faible, propose [Z] même si le score est correct. +3. TYPOLOGIES DE VALEUR : + - [GÉNÉRATEUR DE TRAFIC] : Si le CA est faible mais que le VOLUME est nettement supérieur à la moyenne du groupe/rayon, le produit est stratégique. Il doit être maintenu [A] car il attire le client, même s'il rapporte peu directement. + - [CONTRIBUTEUR DE MARGE] : Si le VOLUME est faible mais que le PMV (Prix Moyen de Vente) et la MARGE sont élevés, le produit est un contributeur de valeur. + - [PRODUIT DE SERVICE] : Produit très régulier (vendu chaque mois) mais à faible enjeux financier. À protéger modérément. +4. VALIDATION PAR LA VALEUR : Pour recommander [A], le produit doit prouver sa valeur réelle. Si les volumes sont dérisoires, le CA faible et la marge faible, propose [Z] même si le score est correct. RÈGLES DE RÉDACTION : 1. VÉRACITÉ : Respecte strictement les chiffres fournis. 2. SANS COMPLAISANCE : Sois directe. Si un produit est mauvais malgré son score, dis-le. -3. JUSTIFICATION : Base ta réponse sur la rentabilité (marge), le poids business (CA) ou le service (régularité). +3. JUSTIFICATION : Base ta réponse sur la rentabilité (marge), le poids business (CA/Volume) ou la typologie (Trafic vs Marge). Options de recommandation : -- [A] - PERMANENT : Produit dont la valeur business ou le service client est prouvé. +- [A] - PERMANENT : Produit dont la valeur business, le trafic ou le service client est prouvé. - [C] - SAISONNIER : Pics de ventes concentrés, inactivité hors saison. - [Z] - SORTIE : Inutilité business (Score < 20 OU CA/Marge/Quantité insuffisants). @@ -24,12 +28,19 @@ Format : "[Recommandation] : [Justification factuelle]"`; } static generateUserMessage(p: ProductAnalysisInput): string { + const pmv = p.totalQuantite > 0 ? p.totalCa / p.totalQuantite : 0; + const monthlySummary = Object.entries(p.sales12m || {}) .map(([k, v]) => `${k}: ${Math.round(v)}u`) .join(", "); const volumeInfo = `Volumes : ${Math.round(p.totalQuantite)}u sur ${p.storeCount} mag.`; + // Contexte comparatif des volumes : On préfère la moyenne du rayon si disponible + const avgRef = p.avgQtyRayon || p.avgQtyFournisseur || 0; + const refName = p.avgQtyRayon ? "rayon" : "fournisseur"; + const relativeVolume = avgRef > 0 ? ` (Moyenne du ${refName} : ${avgRef.toFixed(1)}u)` : ""; + const projectionInfo = p.regularityScore > 0 && p.regularityScore < 12 ? ` --- ANALYSE DE POTENTIEL (Produit récent : ${p.regularityScore}/12 mois active) --- Projection 12 mois : ${Math.round(p.projectedTotalQuantite || 0)}u (${(p.projectedTotalCa || 0).toFixed(2)}€)` : ""; @@ -46,9 +57,10 @@ Projection 12 mois : ${Math.round(p.projectedTotalQuantite || 0)}u (${(p.project DONNÉES BUSINESS : - Marge brute : ${p.tauxMarge.toFixed(1)}% - Total CA : ${p.totalCa.toFixed(2)}€ +- PMV (Prix de Vente Moyen) : ${pmv.toFixed(2)}€ - Score Global App : ${p.score.toFixed(1)}/100 - Régularité : ${p.regularityScore}/12 mois active -${volumeInfo}${projectionInfo}${activityAlert} +${volumeInfo}${relativeVolume}${projectionInfo}${activityAlert} - Historique mensuel (Pondéré) : ${monthlySummary} Analyse la pertinence business du produit sans aucune complaisance. Quelle est ta recommandation (A, C ou Z) ?`; diff --git a/src/features/ai-copilot/components/ai-insight-block.tsx b/src/features/ai-copilot/components/ai-insight-block.tsx index 33c695d..fba165e 100644 --- a/src/features/ai-copilot/components/ai-insight-block.tsx +++ b/src/features/ai-copilot/components/ai-insight-block.tsx @@ -66,6 +66,8 @@ export function AiInsightBlock({ row }: AiInsightBlockProps) { projectedTotalCa: regScore > 0 ? (row.totalCa * weight * (12 / regScore)) : (row.totalCa * weight), lastMonthWithSale: lastMonth, inactivityMonths: inactivity, + avgQtyFournisseur: row.avgQtyFournisseur, + avgQtyRayon: row.avgQtyRayon, }); }; diff --git a/src/features/ai-copilot/models/ai-analysis.types.ts b/src/features/ai-copilot/models/ai-analysis.types.ts index bf1fd44..3ea7e3f 100644 --- a/src/features/ai-copilot/models/ai-analysis.types.ts +++ b/src/features/ai-copilot/models/ai-analysis.types.ts @@ -8,10 +8,8 @@ export interface ProductAnalysisInput { weightedTotalQuantite?: number; weightedTotalCa?: number; avgTotalQuantite?: number; - 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) + avgQtyRayon?: number; + avgQtyFournisseur?: number; storeCount: number; sales12m: Record; codeGamme: string | null; diff --git a/src/features/ai-copilot/store/use-ai-copilot-store.ts b/src/features/ai-copilot/store/use-ai-copilot-store.ts index 7d23a0c..f395e37 100644 --- a/src/features/ai-copilot/store/use-ai-copilot-store.ts +++ b/src/features/ai-copilot/store/use-ai-copilot-store.ts @@ -30,6 +30,8 @@ interface AiCopilotState { projectedTotalCa?: number; lastMonthWithSale?: string; inactivityMonths?: number; + avgQtyFournisseur?: number; + avgQtyRayon?: number; }) => Promise; resetInsights: () => void; } diff --git a/src/lib/score-engine.ts b/src/lib/score-engine.ts index 4da8277..313ef04 100644 --- a/src/lib/score-engine.ts +++ b/src/lib/score-engine.ts @@ -50,8 +50,25 @@ export function computeProductScores( const maxCa = Math.max(...weightedData.map((d) => d.wCa)); const maxMarge = Math.max(...weightedData.map((d) => d.wMarge)); - // 2. Pour chaque produit, calculer le score relatif aux max pondérés + // 2. Calculer les moyennes globales et par rayon + const totalQty = weightedData.reduce((acc, d) => acc + d.wQty, 0); + const avgQtyFournisseur = rows.length > 0 ? totalQty / rows.length : 0; + + const rayonTotals = new Map(); + weightedData.forEach(d => { + const r2 = d.row.code2 || "default"; + const current = rayonTotals.get(r2) || { total: 0, count: 0 }; + rayonTotals.set(r2, { total: current.total + d.wQty, count: current.count + 1 }); + }); + + // 3. Pour chaque produit, calculer le score relatif aux max pondérés et injecter les moyennes for (const data of weightedData) { + // Injection des moyennes pour l'analyse IA + data.row.avgQtyFournisseur = avgQtyFournisseur; + const r2 = data.row.code2 || "default"; + const rayonStat = rayonTotals.get(r2); + data.row.avgQtyRayon = rayonStat ? rayonStat.total / rayonStat.count : 0; + // Score de 0 à 100 sur chaque axe pondéré (le Top 1 pondéré a 100) const scoreQty = maxQty > 0 ? (data.wQty / maxQty) * 100 : 0; const scoreCa = maxCa > 0 ? (data.wCa / maxCa) * 100 : 0; diff --git a/src/types/grid.ts b/src/types/grid.ts index 7572d8f..eb5dba8 100644 --- a/src/types/grid.ts +++ b/src/types/grid.ts @@ -57,6 +57,9 @@ export interface ProductRow { score: number; workingStores: string[]; aiRecommendation?: string | null; + /** Moyennes de comparaison pour l'IA */ + avgQtyFournisseur?: number; + avgQtyRayon?: number; } /** Summary bar totals for the currently visible/filtered rows */ diff --git a/task.md b/task.md index ec1c531..e61a525 100644 --- a/task.md +++ b/task.md @@ -22,8 +22,10 @@ Activation et initialisation de l'Agent Master BMAD. - [x] Étape 14 : Correction du design et du positionnement du modal (z-index/Portal) - [x] Étape 15 : Polissage final et validation Michael - [x] Étape 16 : Correction graphique selon la charte CollectFlow (Slate-950/Apple) +- [x] Étape 17 : Optimisation de l'analyse (Volumes, PMV & Produits de Trafic) +- [x] Étape 18 : Validation finale des recommandations sur cas réels -# Progress Log +## Progress Log - Initialisation de la tâche d'activation de l'Agent Master BMAD. - Variables de session chargées : Michael, French, _bmad-output. @@ -52,3 +54,5 @@ Activation et initialisation de l'Agent Master BMAD. - Refonte visuelle complète du modal : passage aux variables CSS de `globals.css`. - Suppression des bordures blanches et du fond noir pur pour le thème `Slate-950`. - Intégration de `tabular-nums` pour les indicateurs financiers. +- Ajout du calcul du PMV (CA/Qté) et des indicateurs de volume relatif dans l'IA. +- Mise à jour du prompt pour protéger les produits "Générateurs de trafic" (Fort volume, faible CA).