feat: Add bulk AI product analysis functionality with data models, state management, and a UI component.

This commit is contained in:
Michael committed 2026-02-23 00:14:22 +01:00
1 parent 673cab4454
commit 03c5ab6d9e
5 files changed
+26 -4

No files matched your search

@@ -11,9 +11,9 @@ En analysant les données de ventes fournies, génère une recommandation de gam
Critères de pondération et Règles Métier Strictes : Critères de pondération et Règles Métier Strictes :
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. 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). 2. Potentiel Annuel (Run Rate) : Si un produit est récent (Régularité < 12 mois), base ton jugement sur sa "Projection 12m" plutôt que sur son volume brut cumulé.
3. Score de Performance Global (0-100) : Un score > 70 est un indicateur fort pour "Permanent" (A). Un score < 30 est un indicateur fort pour "Sortie" (Z). 3. 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).
4. Régularité des Ventes (0-12 mois) : Un produit avec une vente régulière (> 8 mois/an) est un candidat idéal pour "Permanent" (A). Une régularité faible (< 3 mois) couplée à un volume total faible indique une "Sortie" (Z). 4. Score de Performance Global (0-100) : Un score > 70 est un indicateur fort pour "Permanent" (A). Un score < 30 est un indicateur fort pour "Sortie" (Z).
5. Équilibre : Un produit "A" doit justifier sa place par son flux, sa rentabilité brute OU son score global. 5. Équilibre : Un produit "A" doit justifier sa place par son flux, sa rentabilité brute OU son score global.
Ta réponse doit être courte, directe et sans complaisance. Ta réponse doit être courte, directe et sans complaisance.
@@ -29,6 +29,10 @@ Exemple : "A : Volume de vente et score élevés justifiant le maintien en rayon
const volumeInfo = `Stats Réelles : ${Math.round(p.totalQuantite)}u (${p.totalCa.toFixed(2)}€) sur ${p.storeCount} mag. 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)}€)`; Stats Pondérées (Base 2 mag) : ${Math.round(p.weightedTotalQuantite || 0)}u (${(p.weightedTotalCa || 0).toFixed(2)}€)`;
const projectionInfo = p.regularityScore < 12 ? `
--- ANALYSE DE POTENTIEL (Produit récent : ${p.regularityScore}/12 mois active) ---
Projection 12 mois (Run Rate) : ${Math.round(p.projectedTotalQuantite || 0)}u (${(p.projectedTotalCa || 0).toFixed(2)}€)` : "";
const benchmarks = ` const benchmarks = `
Benchmarks Fournisseur (Global) : Benchmarks Fournisseur (Global) :
- Moyenne 1 mag: ${Math.round(p.avgQtyGroup1 || 0)}u | Multi-mag: ${Math.round(p.avgQtyGroup2 || 0)}u - Moyenne 1 mag: ${Math.round(p.avgQtyGroup1 || 0)}u | Multi-mag: ${Math.round(p.avgQtyGroup2 || 0)}u
@@ -38,7 +42,7 @@ Benchmarks Rayon ("${p.libelleNiveau2}") :
return `Produit : "${p.libelle1}" (Ref: ${p.codein}) return `Produit : "${p.libelle1}" (Ref: ${p.codein})
Rayon : ${p.libelleNiveau2 || "Non classé"} Rayon : ${p.libelleNiveau2 || "Non classé"}
Gamme actuelle : ${p.codeGamme ?? "Non définie"} Gamme actuelle : ${p.codeGamme ?? "Non définie"}
${volumeInfo}${benchmarks} ${volumeInfo}${projectionInfo}${benchmarks}
Indicateurs de Performance : Indicateurs de Performance :
- Score Global App : ${p.score.toFixed(1)}/100 - Score Global App : ${p.score.toFixed(1)}/100
- Régularité des ventes : ${p.regularityScore}/12 mois - Régularité des ventes : ${p.regularityScore}/12 mois
@@ -23,6 +23,9 @@ export function AiInsightBlock({ row }: AiInsightBlockProps) {
// Vider la recommandation actuelle avant de relancer l'IA // Vider la recommandation actuelle avant de relancer l'IA
setDraftGamme(row.codein, "Aucune"); setDraftGamme(row.codein, "Aucune");
const regScore = Object.values(row.sales12m || {}).filter(v => v > 0).length;
const weight = (row.workingStores?.length || 1) === 1 ? 2 : 1;
analyzeProduct({ analyzeProduct({
codein: row.codein, codein: row.codein,
libelle1: row.libelle1, libelle1: row.libelle1,
@@ -32,6 +35,9 @@ export function AiInsightBlock({ row }: AiInsightBlockProps) {
sales12m: row.sales12m, sales12m: row.sales12m,
codeGamme: row.codeGamme, codeGamme: row.codeGamme,
score: row.score, score: row.score,
regularityScore: regScore,
projectedTotalQuantite: regScore > 0 ? (row.totalQuantite * weight * (12 / regScore)) : (row.totalQuantite * weight),
projectedTotalCa: regScore > 0 ? (row.totalCa * weight * (12 / regScore)) : (row.totalCa * weight),
}); });
}; };
@@ -17,6 +17,9 @@ export interface ProductAnalysisInput {
codeGamme: string | null; codeGamme: string | null;
score: number; score: number;
regularityScore: number; regularityScore: number;
/** Projection sur 12 mois si le produit est récent (Run Rate) */
projectedTotalQuantite?: number;
projectedTotalCa?: number;
} }
export interface AnalysisResult { export interface AnalysisResult {
@@ -22,6 +22,9 @@ interface AiCopilotState {
sales12m: Record<string, number>; sales12m: Record<string, number>;
codeGamme: string | null; codeGamme: string | null;
score?: number | null; score?: number | null;
regularityScore?: number;
projectedTotalQuantite?: number;
projectedTotalCa?: number;
}) => Promise<void>; }) => Promise<void>;
resetInsights: () => void; resetInsights: () => void;
} }
@@ -82,6 +82,12 @@ export function BulkAiAnalyzer() {
codeGamme: r.codeGamme || null, codeGamme: r.codeGamme || null,
score: r.score || 0, score: r.score || 0,
regularityScore: Object.values(r.sales12m || {}).filter(v => v > 0).length, regularityScore: Object.values(r.sales12m || {}).filter(v => v > 0).length,
projectedTotalQuantite: (Object.values(r.sales12m || {}).filter(v => v > 0).length > 0)
? ((r.totalQuantite || 0) * weight * (12 / Object.values(r.sales12m || {}).filter(v => v > 0).length))
: ((r.totalQuantite || 0) * weight),
projectedTotalCa: (Object.values(r.sales12m || {}).filter(v => v > 0).length > 0)
? ((r.totalCa || 0) * weight * (12 / Object.values(r.sales12m || {}).filter(v => v > 0).length))
: ((r.totalCa || 0) * weight),
}; };
}); });