mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat(ai): performance-first logic with store awareness extrapolation and change count fix
This commit is contained in:
1 parent
c5cacd9b5a
commit
259fb60638
3 files changed
+33
-16
No files matched your search
@@ -16,6 +16,7 @@ const BatchAnalyzeSchema = z.object({
|
|||||||
score: z.number().nullable().optional(),
|
score: z.number().nullable().optional(),
|
||||||
codeGamme: z.string().nullable().optional(),
|
codeGamme: z.string().nullable().optional(),
|
||||||
sales12m: z.record(z.string(), z.number()).nullable().optional(),
|
sales12m: z.record(z.string(), z.number()).nullable().optional(),
|
||||||
|
storeCount: z.number().optional().default(1),
|
||||||
nomenclature: z.string().nullable().optional(),
|
nomenclature: z.string().nullable().optional(),
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
@@ -56,24 +57,26 @@ ${supplierMetricsContext}
|
|||||||
|
|
||||||
MÉTHODOLOGIE D'ANALYSE (APPROCHE PERFORMANCE-FIRST) :
|
MÉTHODOLOGIE D'ANALYSE (APPROCHE PERFORMANCE-FIRST) :
|
||||||
|
|
||||||
1. CRITÈRE PRINCIPAL - LA SANTÉ (SCORE) :
|
1. ANALYSE DU POTENTIEL (RÈGLE DU MULTI-MAGASIN) :
|
||||||
|
- Note le \`storeCount\` de chaque produit.
|
||||||
|
- La majorité des produits sont présents dans 2 magasins. Si un produit a \`storeCount = 1\`, ses données (CA, Ventes, Marge) sont mécaniquement plus faibles car il ne travaille que sur une partie du réseau.
|
||||||
|
- **RÈGLE CRITIQUE** : Pour un produit mono-magasin (\`storeCount = 1\`), multiplie mentalement ses indicateurs par **1.7x** pour évaluer son potentiel réel s'il était déployé partout. Compare-le aux autres produits APRES cette extrapolation.
|
||||||
|
|
||||||
|
2. CRITÈRE PRINCIPAL - LA SANTÉ (SCORE) :
|
||||||
- Le Score (0-100) est ton indicateur de performance combiné (rotation, rentabilité relative, tendance).
|
- Le Score (0-100) est ton indicateur de performance combiné (rotation, rentabilité relative, tendance).
|
||||||
- Score > 70 : Produit performant, Gamme A par défaut.
|
- Score > 70 : Produit performant, Gamme A par défaut.
|
||||||
- Score < 30 : Produit en difficulté, cible prioritaire pour la Gamme Z.
|
- Score < 30 : Produit en difficulté, cible prioritaire pour la Gamme Z (sauf si mono-magasin avec fort potentiel extrapolé).
|
||||||
|
|
||||||
2. LE POIDS COMME BOUCLIER (PROTECTION DES PILIERS) :
|
3. LE POIDS COMME BOUCLIER (PROTECTION DES PILIERS) :
|
||||||
- Le poids (CA et Marge du produit par rapport aux totaux du fournisseur) n'est PAS un critère automatique de maintien.
|
- Un produit avec un MAUVAIS SCORE (< 40) ne reste en Gamme A QUE s'il est un pilier indispensable :
|
||||||
- Si un produit a un MAUVAIS SCORE (< 40), tu ne le gardes en Gamme A QUE s'il est un pilier indispensable :
|
- Son poids CA/Marge (extrapolé si mono-magasin) est substantiel par rapport au business du fournisseur.
|
||||||
- Il porte une part substantielle du business du fournisseur (poids CA ou Marge élevé par rapport aux autres).
|
- OU il génère un flux de volume massif indispensable.
|
||||||
- OU il génère un flux de volume (unités) massif indispensable au rayon.
|
|
||||||
- IMPORTANT : Dans un catalogue de milliers de produits, un "pilier" peut avoir un faible pourcentage absolu (ex: 0.1%). Dans un petit catalogue, il doit peser lourd. Sois agnostique à la taille du fournisseur.
|
|
||||||
|
|
||||||
3. ARBITRAGE POUR LA SORTIE (GAMME Z) :
|
4. ARBITRAGE POUR LA SORTIE (GAMME Z) :
|
||||||
- Propose la Gamme Z si le produit cumule :
|
- Propose la Gamme Z si le produit cumule :
|
||||||
- Score faible (< 40).
|
- Score faible (< 40).
|
||||||
- ET Contribution négligeable au business global (ne "déplace pas l'aiguille" du CA ou de la Marge du fournisseur).
|
- ET Contribution négligeable au business global (même après extrapolation 1.7x si mono-magasin).
|
||||||
- ET Ventes sporadiques ou inexistantes sur les derniers mois.
|
- ET Ventes sporadiques.
|
||||||
- GARDE-FOU : Si Score < 20 ET que le produit n'est manifestement pas un top contributeur, le passage en Z est impératif.
|
|
||||||
|
|
||||||
DÉFINITION DES GAMMES :
|
DÉFINITION DES GAMMES :
|
||||||
- A (Permanent) : Produits sains (Score élevé) OU Piliers business indispensables (gros volume/CA malgré score moyen).
|
- A (Permanent) : Produits sains (Score élevé) OU Piliers business indispensables (gros volume/CA malgré score moyen).
|
||||||
@@ -81,7 +84,7 @@ DÉFINITION DES GAMMES :
|
|||||||
- Z (Sortie) : Produits non performants et non stratégiques.
|
- Z (Sortie) : Produits non performants et non stratégiques.
|
||||||
|
|
||||||
IMPORTANT : RÉPONDS UNIQUEMENT EN JSON VALIDE.
|
IMPORTANT : RÉPONDS UNIQUEMENT EN JSON VALIDE.
|
||||||
Ta justification doit expliquer POURQUOI le produit est maintenu ou sorti en croisant Score et Contribution Réelle au business global. Mentionne le poids CA fournisseur explicitement.
|
Ta justification doit expliquer POURQUOI le produit est maintenu ou sorti. Mentionne explicitement si le produit est pénalisé par sa présence dans un seul magasin (ex: "Mono-magasin : potentiel extrapolé de X€, pilier stratégique").
|
||||||
|
|
||||||
Format:
|
Format:
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ export function BulkAiAnalyzer() {
|
|||||||
marge: r.totalMarge ? parseFloat(((r.totalMarge / (r.totalCa || 1)) * 100).toFixed(1)) : 0,
|
marge: r.totalMarge ? parseFloat(((r.totalMarge / (r.totalCa || 1)) * 100).toFixed(1)) : 0,
|
||||||
score: r.score || 0,
|
score: r.score || 0,
|
||||||
codeGamme: r.codeGamme || "N/A",
|
codeGamme: r.codeGamme || "N/A",
|
||||||
sales12m: r.sales12m || {}, // Send raw object for better AI analysis
|
sales12m: r.sales12m || {},
|
||||||
|
storeCount: r.workingStores?.length || 1,
|
||||||
nomenclature: `${r.libelleNiveau1 || ""} > ${r.libelleNiveau2 || ""} > ${r.libelle3 || ""}`
|
nomenclature: `${r.libelleNiveau1 || ""} > ${r.libelleNiveau2 || ""} > ${r.libelle3 || ""}`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,21 @@ export const useGridStore = create<GridState>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
setDraftGamme: (codein, gamme) => {
|
setDraftGamme: (codein, gamme) => {
|
||||||
const draftChanges = { ...get().draftChanges, [codein]: gamme };
|
const { rows, draftChanges: oldDrafts } = get();
|
||||||
set({ draftChanges, summary: computeSummary(get().rows, draftChanges) });
|
const originalRow = rows.find(r => r.codein === codein);
|
||||||
|
const originalGamme = originalRow?.codeGamme;
|
||||||
|
|
||||||
|
const draftChanges = { ...oldDrafts };
|
||||||
|
|
||||||
|
if (gamme === originalGamme) {
|
||||||
|
// If the new value matches initial, remove it from drafts
|
||||||
|
delete draftChanges[codein];
|
||||||
|
} else {
|
||||||
|
// Otherwise, record the change
|
||||||
|
draftChanges[codein] = gamme;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ draftChanges, summary: computeSummary(rows, draftChanges) });
|
||||||
},
|
},
|
||||||
|
|
||||||
resetDrafts: () => {
|
resetDrafts: () => {
|
||||||
|
|||||||
Reference in new issue
Block a user