mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +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 targetQtyPerStore = normQty(target);
|
||||
|
||||
// 1. Totaux fournisseur (valeurs brutes pour les poids de représentativité réseau)
|
||||
const totalCaFournisseur = allProds.reduce((s, p) => s + (p.totalCa ?? 0), 0);
|
||||
const totalQtyFournisseur = allProds.reduce((s, p) => s + (p.totalQuantite ?? 0), 0);
|
||||
// 1. Totaux fournisseur (valeurs PONDÉRÉES pour comparaison équitable réseau)
|
||||
// weightedTotalCa/Qty projettent les 1-magasin sur une base réseau (×2).
|
||||
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)
|
||||
const targetRayonKey = getRayonKey(target);
|
||||
const rayonProds = allProds.filter(p => getRayonKey(p) === targetRayonKey);
|
||||
const totalCaRayon = rayonProds.reduce((s, p) => s + (p.totalCa ?? 0), 0);
|
||||
const totalQtyRayon = rayonProds.reduce((s, p) => s + (p.totalQuantite ?? 0), 0);
|
||||
const totalCaRayon = rayonProds.reduce((s, p) => s + getWeightedCa(p), 0);
|
||||
const totalQtyRayon = rayonProds.reduce((s, p) => s + getWeightedQty(p), 0);
|
||||
|
||||
// 3. Distributions normalisées (PAR MAGASIN) — pour les percentiles et le quadrant
|
||||
const allCaPerStore = allProds.map(normCa);
|
||||
@@ -209,14 +213,14 @@ export class ContextProfiler {
|
||||
const isTop20Qty = targetQtyPerStore >= top20QtyThreshold;
|
||||
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 =
|
||||
totalCaFournisseur > 0
|
||||
? Math.round(((target.totalCa ?? 0) / totalCaFournisseur) * 1000) / 10
|
||||
? Math.round((getWeightedCa(target) / totalCaFournisseur) * 1000) / 10
|
||||
: 0;
|
||||
const weightQtyFournisseur =
|
||||
totalQtyFournisseur > 0
|
||||
? Math.round(((target.totalQuantite ?? 0) / totalQtyFournisseur) * 1000) / 10
|
||||
? Math.round((getWeightedQty(target) / totalQtyFournisseur) * 1000) / 10
|
||||
: 0;
|
||||
|
||||
const isLowContribution = weightCaFournisseur < 0.5 && weightQtyFournisseur < 0.5;
|
||||
@@ -289,11 +293,11 @@ export class ContextProfiler {
|
||||
weightQtyFournisseur,
|
||||
weightCaRayon:
|
||||
totalCaRayon > 0
|
||||
? Math.round(((target.totalCa ?? 0) / totalCaRayon) * 1000) / 10
|
||||
? Math.round((getWeightedCa(target) / totalCaRayon) * 1000) / 10
|
||||
: 0,
|
||||
weightQtyRayon:
|
||||
totalQtyRayon > 0
|
||||
? Math.round(((target.totalQuantite ?? 0) / totalQtyRayon) * 1000) / 10
|
||||
? Math.round((getWeightedQty(target) / totalQtyRayon) * 1000) / 10
|
||||
: 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) {
|
||||
const coef = (p.totalMagasins || 1) / (p.storeCount || 1);
|
||||
let caNorm = (p.totalCa || 0) * coef;
|
||||
let volNorm = (p.totalQuantite || 0) * coef;
|
||||
// Valeurs pondérées réseau — si absentes, fallback sur les brutes
|
||||
let caNorm = p.weightedTotalCa ?? p.totalCa ?? 0;
|
||||
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) {
|
||||
caNorm = (caNorm / (p.regularityScore || 3)) * 12;
|
||||
volNorm = (volNorm / (p.regularityScore || 3)) * 12;
|
||||
|
||||
Reference in new issue
Block a user