mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-23 23:18:10 +02:00
feat: create ScoringEngine for AI copilot product analysis and scoring.
This commit is contained in:
1 parent
a053556a64
commit
5773ed9a26
2 files changed
+29
-14
No files matched your search
@@ -182,15 +182,19 @@ export class ContextProfiler {
|
|||||||
const targetCaPerStore = normCa(target);
|
const targetCaPerStore = normCa(target);
|
||||||
const targetQtyPerStore = normQty(target);
|
const targetQtyPerStore = normQty(target);
|
||||||
|
|
||||||
// 1. Totaux fournisseur (valeurs brutes pour les poids de représentativité réseau)
|
// 1. Totaux fournisseur (valeurs PONDÉRÉES pour comparaison équitable réseau)
|
||||||
const totalCaFournisseur = allProds.reduce((s, p) => s + (p.totalCa ?? 0), 0);
|
// weightedTotalCa/Qty projettent les 1-magasin sur une base réseau (×2).
|
||||||
const totalQtyFournisseur = allProds.reduce((s, p) => s + (p.totalQuantite ?? 0), 0);
|
const getWeightedCa = (p: ProductAnalysisInput) => p.weightedTotalCa ?? p.totalCa ?? 0;
|
||||||
|
const getWeightedQty = (p: ProductAnalysisInput) => p.weightedTotalQuantite ?? p.totalQuantite ?? 0;
|
||||||
|
|
||||||
|
const totalCaFournisseur = allProds.reduce((s, p) => s + getWeightedCa(p), 0);
|
||||||
|
const totalQtyFournisseur = allProds.reduce((s, p) => s + getWeightedQty(p), 0);
|
||||||
|
|
||||||
// 2. Totaux du rayon (Niveau 2 de nomenclature)
|
// 2. Totaux du rayon (Niveau 2 de nomenclature)
|
||||||
const targetRayonKey = getRayonKey(target);
|
const targetRayonKey = getRayonKey(target);
|
||||||
const rayonProds = allProds.filter(p => getRayonKey(p) === targetRayonKey);
|
const rayonProds = allProds.filter(p => getRayonKey(p) === targetRayonKey);
|
||||||
const totalCaRayon = rayonProds.reduce((s, p) => s + (p.totalCa ?? 0), 0);
|
const totalCaRayon = rayonProds.reduce((s, p) => s + getWeightedCa(p), 0);
|
||||||
const totalQtyRayon = rayonProds.reduce((s, p) => s + (p.totalQuantite ?? 0), 0);
|
const totalQtyRayon = rayonProds.reduce((s, p) => s + getWeightedQty(p), 0);
|
||||||
|
|
||||||
// 3. Distributions normalisées (PAR MAGASIN) — pour les percentiles et le quadrant
|
// 3. Distributions normalisées (PAR MAGASIN) — pour les percentiles et le quadrant
|
||||||
const allCaPerStore = allProds.map(normCa);
|
const allCaPerStore = allProds.map(normCa);
|
||||||
@@ -209,14 +213,14 @@ export class ContextProfiler {
|
|||||||
const isTop20Qty = targetQtyPerStore >= top20QtyThreshold;
|
const isTop20Qty = targetQtyPerStore >= top20QtyThreshold;
|
||||||
const isAboveMedianComposite = pComposite >= 50;
|
const isAboveMedianComposite = pComposite >= 50;
|
||||||
|
|
||||||
// 5. Poids bruts (valeurs réseau pour représentativité commerciale réelle)
|
// 5. Poids pondérés (valeurs réseau projetées pour représentativité réaliste)
|
||||||
const weightCaFournisseur =
|
const weightCaFournisseur =
|
||||||
totalCaFournisseur > 0
|
totalCaFournisseur > 0
|
||||||
? Math.round(((target.totalCa ?? 0) / totalCaFournisseur) * 1000) / 10
|
? Math.round((getWeightedCa(target) / totalCaFournisseur) * 1000) / 10
|
||||||
: 0;
|
: 0;
|
||||||
const weightQtyFournisseur =
|
const weightQtyFournisseur =
|
||||||
totalQtyFournisseur > 0
|
totalQtyFournisseur > 0
|
||||||
? Math.round(((target.totalQuantite ?? 0) / totalQtyFournisseur) * 1000) / 10
|
? Math.round((getWeightedQty(target) / totalQtyFournisseur) * 1000) / 10
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const isLowContribution = weightCaFournisseur < 0.5 && weightQtyFournisseur < 0.5;
|
const isLowContribution = weightCaFournisseur < 0.5 && weightQtyFournisseur < 0.5;
|
||||||
@@ -289,11 +293,11 @@ export class ContextProfiler {
|
|||||||
weightQtyFournisseur,
|
weightQtyFournisseur,
|
||||||
weightCaRayon:
|
weightCaRayon:
|
||||||
totalCaRayon > 0
|
totalCaRayon > 0
|
||||||
? Math.round(((target.totalCa ?? 0) / totalCaRayon) * 1000) / 10
|
? Math.round((getWeightedCa(target) / totalCaRayon) * 1000) / 10
|
||||||
: 0,
|
: 0,
|
||||||
weightQtyRayon:
|
weightQtyRayon:
|
||||||
totalQtyRayon > 0
|
totalQtyRayon > 0
|
||||||
? Math.round(((target.totalQuantite ?? 0) / totalQtyRayon) * 1000) / 10
|
? Math.round((getWeightedQty(target) / totalQtyRayon) * 1000) / 10
|
||||||
: 0,
|
: 0,
|
||||||
|
|
||||||
tauxMarge: target.tauxMarge ?? 0,
|
tauxMarge: target.tauxMarge ?? 0,
|
||||||
|
|||||||
@@ -131,12 +131,23 @@ export class ScoringEngine {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalise les KPIs d'un produit pour comparaison équitable.
|
||||||
|
*
|
||||||
|
* Utilise les valeurs pondérées (weightedTotalCa / weightedTotalQuantite)
|
||||||
|
* qui projettent les produits 1-magasin sur une base réseau comparable
|
||||||
|
* (×2 si 1 magasin, ×1 si 2 magasins). Cela évite de pénaliser un
|
||||||
|
* produit vendu uniquement dans 1 magasin face à un produit vendu dans 2.
|
||||||
|
*
|
||||||
|
* Applique ensuite la projection Run Rate pour les produits récents
|
||||||
|
* (\< 12 mois d'historique) afin de comparer sur une base annuelle.
|
||||||
|
*/
|
||||||
private static preprocess(p: ProductAnalysisInput) {
|
private static preprocess(p: ProductAnalysisInput) {
|
||||||
const coef = (p.totalMagasins || 1) / (p.storeCount || 1);
|
// Valeurs pondérées réseau — si absentes, fallback sur les brutes
|
||||||
let caNorm = (p.totalCa || 0) * coef;
|
let caNorm = p.weightedTotalCa ?? p.totalCa ?? 0;
|
||||||
let volNorm = (p.totalQuantite || 0) * coef;
|
let volNorm = p.weightedTotalQuantite ?? p.totalQuantite ?? 0;
|
||||||
|
|
||||||
// Run Rate Projection
|
// Run Rate Projection (produits avec 3-11 mois d'historique)
|
||||||
if ((p.regularityScore || 0) >= 3 && (p.regularityScore || 0) < 12) {
|
if ((p.regularityScore || 0) >= 3 && (p.regularityScore || 0) < 12) {
|
||||||
caNorm = (caNorm / (p.regularityScore || 3)) * 12;
|
caNorm = (caNorm / (p.regularityScore || 3)) * 12;
|
||||||
volNorm = (volNorm / (p.regularityScore || 3)) * 12;
|
volNorm = (volNorm / (p.regularityScore || 3)) * 12;
|
||||||
|
|||||||
Reference in new issue
Block a user