mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-22 22:48:08 +02:00
feat: Introduce AI Copilot for bulk product analysis, including a dedicated store, UI components, and business logic.
This commit is contained in:
1 parent
03c5ab6d9e
commit
8c02771e46
7 files changed
+308
-121
No files matched your search
@@ -12,9 +12,11 @@ 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 :
|
||||
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. 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. 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. 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.
|
||||
3. Détection Saisonnalité/Fin de vie : Si un produit est inactif depuis plus de 2 mois (Inactivité > 2), la projection 12 mois devient INCERTAINE.
|
||||
- Si Inactivité > 2 et Régularité < 6 : Probable SAISONNIER [C] ou SORTIE [Z].
|
||||
- Si Inactivité <= 2 et Régularité < 12 : Probable LANCEMENT (Permanent [A]).
|
||||
4. 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).
|
||||
5. 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).
|
||||
|
||||
Ta réponse doit être courte, directe et sans complaisance.
|
||||
Format impératif : "[Recommandation] : [Texte brut de l'explication sans aucun préfixe du type 'Justification:' ou 'Pourquoi:']"
|
||||
@@ -22,7 +24,7 @@ Exemple : "A : Volume de vente et score élevés justifiant le maintien en rayon
|
||||
}
|
||||
|
||||
static generateUserMessage(p: ProductAnalysisInput): string {
|
||||
const monthlySummary = Object.entries(p.sales12m)
|
||||
const monthlySummary = Object.entries(p.sales12m || {})
|
||||
.map(([k, v]) => `${k}: ${Math.round(v)}u`)
|
||||
.join(", ");
|
||||
|
||||
@@ -33,6 +35,10 @@ Stats Pondérées (Base 2 mag) : ${Math.round(p.weightedTotalQuantite || 0)}u ($
|
||||
--- 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 activityAlert = (p.inactivityMonths || 0) > 2
|
||||
? `\n⚠️ ALERTE : Inactif depuis ${p.inactivityMonths} mois (Dernière vente: ${p.lastMonthWithSale || "Inconnue"})`
|
||||
: "";
|
||||
|
||||
const benchmarks = `
|
||||
Benchmarks Fournisseur (Global) :
|
||||
- Moyenne 1 mag: ${Math.round(p.avgQtyGroup1 || 0)}u | Multi-mag: ${Math.round(p.avgQtyGroup2 || 0)}u
|
||||
@@ -42,7 +48,7 @@ Benchmarks Rayon ("${p.libelleNiveau2}") :
|
||||
return `Produit : "${p.libelle1}" (Ref: ${p.codein})
|
||||
Rayon : ${p.libelleNiveau2 || "Non classé"}
|
||||
Gamme actuelle : ${p.codeGamme ?? "Non définie"}
|
||||
${volumeInfo}${projectionInfo}${benchmarks}
|
||||
${volumeInfo}${projectionInfo}${activityAlert}${benchmarks}
|
||||
Indicateurs de Performance :
|
||||
- Score Global App : ${p.score.toFixed(1)}/100
|
||||
- Régularité des ventes : ${p.regularityScore}/12 mois
|
||||
|
||||
@@ -26,6 +26,25 @@ export function AiInsightBlock({ row }: AiInsightBlockProps) {
|
||||
const regScore = Object.values(row.sales12m || {}).filter(v => v > 0).length;
|
||||
const weight = (row.workingStores?.length || 1) === 1 ? 2 : 1;
|
||||
|
||||
// Calcul de l'inactivité (Récence)
|
||||
const allMonths = Object.keys(row.sales12m || {});
|
||||
const referenceMonth = allMonths.length > 0 ? Math.max(...allMonths.map(m => parseInt(m))).toString() : "";
|
||||
|
||||
const salesMonths = Object.entries(row.sales12m || {})
|
||||
.filter(([_, qty]) => qty > 0)
|
||||
.map(([m]) => parseInt(m))
|
||||
.sort((a, b) => b - a);
|
||||
|
||||
const lastMonth = salesMonths.length > 0 ? salesMonths[0].toString() : "";
|
||||
let inactivity = 0;
|
||||
if (referenceMonth && lastMonth) {
|
||||
const refY = parseInt(referenceMonth.substring(0, 4));
|
||||
const refM = parseInt(referenceMonth.substring(4, 6));
|
||||
const lastY = parseInt(lastMonth.substring(0, 4));
|
||||
const lastM = parseInt(lastMonth.substring(4, 6));
|
||||
inactivity = (refY - lastY) * 12 + (refM - lastM);
|
||||
}
|
||||
|
||||
analyzeProduct({
|
||||
codein: row.codein,
|
||||
libelle1: row.libelle1,
|
||||
@@ -38,6 +57,8 @@ export function AiInsightBlock({ row }: AiInsightBlockProps) {
|
||||
regularityScore: regScore,
|
||||
projectedTotalQuantite: regScore > 0 ? (row.totalQuantite * weight * (12 / regScore)) : (row.totalQuantite * weight),
|
||||
projectedTotalCa: regScore > 0 ? (row.totalCa * weight * (12 / regScore)) : (row.totalCa * weight),
|
||||
lastMonthWithSale: lastMonth,
|
||||
inactivityMonths: inactivity,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ export interface ProductAnalysisInput {
|
||||
/** Projection sur 12 mois si le produit est récent (Run Rate) */
|
||||
projectedTotalQuantite?: number;
|
||||
projectedTotalCa?: number;
|
||||
/** Analyse de saisonnalité */
|
||||
lastMonthWithSale?: string;
|
||||
inactivityMonths?: number;
|
||||
}
|
||||
|
||||
export interface AnalysisResult {
|
||||
|
||||
@@ -13,6 +13,8 @@ interface AiInsight {
|
||||
interface AiCopilotState {
|
||||
insights: Record<string, AiInsight>;
|
||||
setInsight: (codein: string, insight: string, isDuplicate?: boolean) => void;
|
||||
setLoading: (codein: string) => void;
|
||||
setError: (codein: string, error: string) => void;
|
||||
analyzeProduct: (payload: {
|
||||
codein: string;
|
||||
libelle1: string;
|
||||
@@ -25,6 +27,8 @@ interface AiCopilotState {
|
||||
regularityScore?: number;
|
||||
projectedTotalQuantite?: number;
|
||||
projectedTotalCa?: number;
|
||||
lastMonthWithSale?: string;
|
||||
inactivityMonths?: number;
|
||||
}) => Promise<void>;
|
||||
resetInsights: () => void;
|
||||
}
|
||||
@@ -43,6 +47,24 @@ export const useAiCopilotStore = create<AiCopilotState>()(
|
||||
}));
|
||||
},
|
||||
|
||||
setLoading: (codein) => {
|
||||
set((s) => ({
|
||||
insights: {
|
||||
...s.insights,
|
||||
[codein]: { codein, insight: "", status: "loading" },
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
setError: (codein, error) => {
|
||||
set((s) => ({
|
||||
insights: {
|
||||
...s.insights,
|
||||
[codein]: { codein, insight: error, status: "error" },
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
resetInsights: () => {
|
||||
set({ insights: {} });
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user