feat: Add bulk AI product analysis component with supporting data types and AI system prompt.

This commit is contained in:
Michael committed 2026-02-22 12:11:32 +01:00
1 parent b015d1f093
commit 661704bf38
3 files changed
+16 -4

No files matched your search

@@ -13,7 +13,8 @@ Critères de pondération et Règles Métier :
1. Volume Relatif (Flux de stock) : Si le volume est élevé (très supérieur à la moyenne), c'est un produit "fond de rayon" qui génère du trafic. ON LE GARDE (A ou C) même si son CA individuel est faible.
2. Mix CA/Marge (Rentabilité) : Si le volume est faible mais que le produit génère un CA conséquent et une BONNE MARGE, c'est une pépite. ON LE GARDE EN A.
3. Poids Relatif : Analyse toujours la performance du produit PAR RAPPORT AU RESTE (moyenne fournisseur). Un produit doit être Z uniquement s'il est faible sur TOUS les indicateurs (Volume, CA, Marge).
4. Régularité : La stabilité sur 12 mois est la signature du Permanent (A).
4. Contexte Magasins (Crucial) : Compare le produit à ses pairs. Si le produit est dans 1 magasin, compare son volume à la moyenne des produits à 1 magasin (avgQtyGroup1). Ne le pénalise pas parce qu'il vend moins qu'un produit présent dans 2 magasins ou plus. Un produit à 1 magasin qui performe mieux que la moyenne de son groupe est un excellent candidat A.
5. Régularité : La stabilité sur 12 mois est la signature du Permanent (A).
Ta réponse doit être en 1-2 phrases maximum, en français, directe et actionnable.
Format impératif : "[Recommandation]: [Justification courte]"`;
@@ -30,6 +31,7 @@ Format impératif : "[Recommandation]: [Justification courte]"`;
return `Produit : "${p.libelle1}" (Ref: ${p.codein})
Gamme actuelle : ${p.codeGamme ?? "Non définie"}
Contexte : Données basées sur ${p.storeCount} magasin(s).
Indicateurs 12m :
- CA : ${p.totalCa.toFixed(2)}€
- Marge : ${p.tauxMarge.toFixed(1)}%
@@ -5,6 +5,9 @@ export interface ProductAnalysisInput {
tauxMarge: number;
totalQuantite: number;
avgTotalQuantite?: number;
avgQtyGroup1?: number; // Moyenne volume pour produits à 1 magasin
avgQtyGroup2?: number; // Moyenne volume pour produits à 2 magasins+
storeCount: number;
sales12m: Record<string, number>;
codeGamme: string | null;
}
@@ -20,12 +20,16 @@ export function BulkAiAnalyzer() {
};
const handleAnalyze = async () => {
const { rows } = useGridStore.getState();
isCancelledRef.current = false;
const totalQtySum = rows.reduce((sum, r) => sum + (r.totalQuantite || 0), 0);
const avgQty = rows.length > 0 ? totalQtySum / rows.length : 0;
// Benchmarks par groupe de magasins
const rowsGroup1 = rows.filter(r => (r.workingStores?.length || 1) === 1);
const rowsGroup2 = rows.filter(r => (r.workingStores?.length || 1) >= 2);
const avgQty1 = rowsGroup1.length > 0 ? rowsGroup1.reduce((s, r) => s + (r.totalQuantite || 0), 0) / rowsGroup1.length : 0;
const avgQty2 = rowsGroup2.length > 0 ? rowsGroup2.reduce((s, r) => s + (r.totalQuantite || 0), 0) / rowsGroup2.length : 0;
const productPayloads: ProductAnalysisInput[] = rows.map(r => ({
codein: r.codein,
libelle1: r.libelle1 || "",
@@ -33,6 +37,9 @@ export function BulkAiAnalyzer() {
tauxMarge: r.tauxMarge || 0,
totalQuantite: r.totalQuantite || 0,
avgTotalQuantite: avgQty,
avgQtyGroup1: avgQty1,
avgQtyGroup2: avgQty2,
storeCount: r.workingStores?.length || 1,
sales12m: r.sales12m || {},
codeGamme: r.codeGamme || null,
}));