From 9d8ef0e4db5c97d568bc338e8f183307b3982ba8 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Thu, 26 Feb 2026 15:21:16 +0100 Subject: [PATCH] feat: Implement dashboard layout, shared sidebar, AI copilot analysis and scoring engines, and a grid floating summary bar. --- src/app/(dashboard)/layout.tsx | 2 +- src/components/shared/sidebar.tsx | 91 ++++++++++++++++--- .../ai-copilot/business/analysis-engine.ts | 7 +- .../ai-copilot/business/scoring-engine.ts | 6 ++ .../grid/components/floating-summary-bar.tsx | 21 +++-- 5 files changed, 100 insertions(+), 27 deletions(-) diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index ff27880..9827a52 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -5,7 +5,7 @@ export default async function DashboardLayout({ children }: { children: React.Re return (
-
+
{children} diff --git a/src/components/shared/sidebar.tsx b/src/components/shared/sidebar.tsx index a8ff952..262630c 100644 --- a/src/components/shared/sidebar.tsx +++ b/src/components/shared/sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { LayoutGrid, Camera, FileDown, Settings, Package, BarChart3, LogOut, User as UserIcon, Loader2, Bot } from "lucide-react"; +import { LayoutGrid, Camera, FileDown, Settings, Package, BarChart3, LogOut, User as UserIcon, Loader2, Bot, ChevronRight, ChevronLeft } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { cn } from "@/lib/utils"; @@ -22,28 +22,56 @@ export function Sidebar() { const { data: session } = useSession(); const activeGridQuery = useGridStore((s) => s.activeGridQuery); const [isMounted, setIsMounted] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); useEffect(() => { setIsMounted(true); + // Load expanded state from local storage + const stored = localStorage.getItem("sidebar_expanded"); + if (stored === "true") setIsExpanded(true); }, []); + const toggleSidebar = () => { + const next = !isExpanded; + setIsExpanded(next); + localStorage.setItem("sidebar_expanded", String(next)); + }; + const userRole = (session?.user as any)?.role; const filteredItems = NAV_ITEMS.filter(item => !item.adminOnly || userRole === "admin"); return ( diff --git a/src/features/ai-copilot/business/analysis-engine.ts b/src/features/ai-copilot/business/analysis-engine.ts index 21662f1..d034eeb 100644 --- a/src/features/ai-copilot/business/analysis-engine.ts +++ b/src/features/ai-copilot/business/analysis-engine.ts @@ -15,13 +15,14 @@ Le score (0 à 100) est une mesure de performance RELATIVE au sein du rayon. --- TON RÔLE --- 1. JUSTIFIER : Explique le score de manière purement factuelle, "ligne par ligne", en te basant UNIQUEMENT sur : le CA, la Quantité vendue, la Marge, le Poids Fournisseur (importance du produit pour ce fournisseur) et le Poids Rayon / Nomenclature (importance du produit dans le rayon). 2. EXPLIQUER LE VERDICT [A] : Si le verdict est [A] malgré un score faible, c'est une PROTECTION MÉTIER (ex: Produit Récent, Leader Fournisseur, Dernier Produit). Explique cette protection avec bienveillance. -3. INTERDICTION : Ne mentionne JAMAIS de tendances mensuelles, historiques, prédictions ou notions de temps. Ne calcule pas de variations. Reste strictement concentrée sur la photographie des indicateurs globaux fournis. -4. CONCISE : 2 phrases maximum. Évite le jargon de data-scientist. +3. EXPLIQUER LE VERDICT [Z] : Si le verdict est [Z] malgré de bons KPIs, c'est souvent dû à une RÈGLE D'EXCLUSION ABSOLUE (ex: Note Globale < 20). Justifie dans ce cas par l'insuffisance critique de la performance globale du produit. +4. INTERDICTION : Ne mentionne JAMAIS de tendances mensuelles, historiques, prédictions ou notions de temps. Ne calcule pas de variations. Reste strictement concentrée sur la photographie des indicateurs globaux fournis. +5. CONCISE : 2 phrases maximum. Évite le jargon de data-scientist. --- FORMAT ATTENDU --- "[Recommandation] : [Justification factuelle incluant le Score et les poids/axes clés]" Exemple : "[A] : Score de 85/100 porté par une forte marge (45%) et un poids important dans le rayon (15% des quantités)." -Exemple : "[Z] : Score très faible (12/100) en raison d'un CA marginal malgré un bon taux de marge, le produit pesant moins de 1% chez son fournisseur."`; +Exemple : "[Z] : Rétrogradé car la note globale (15/100) est critique, malgré un volume de vente acceptable au sein du rayon."`; } static generateUserMessage(p: ProductAnalysisInput): string { diff --git a/src/features/ai-copilot/business/scoring-engine.ts b/src/features/ai-copilot/business/scoring-engine.ts index 44853b2..95aa1b6 100644 --- a/src/features/ai-copilot/business/scoring-engine.ts +++ b/src/features/ai-copilot/business/scoring-engine.ts @@ -106,6 +106,12 @@ export class ScoringEngine { finalLabel = "Dernière Réf Fournisseur (Protégée)"; } + // Règle d'exclusion absolue (Demande Utilisateur) : Aucun produit de note < 20 en Gamme A + if (target.score !== undefined && target.score < 20) { + recommendation = "Z"; + finalLabel = "Note Globale Critique (< 20)"; + } + return { compositeScore: Math.round(compositeScore * 100), percentiles: { ca: pCa, volume: pVol, marge: pMarge }, diff --git a/src/features/grid/components/floating-summary-bar.tsx b/src/features/grid/components/floating-summary-bar.tsx index cb6acf9..156e41d 100644 --- a/src/features/grid/components/floating-summary-bar.tsx +++ b/src/features/grid/components/floating-summary-bar.tsx @@ -114,28 +114,33 @@ export function FloatingSummaryBar() { borderColor: hasDrafts ? "rgba(234, 179, 8, 0.5)" : "var(--border-strong)", }} > -
+
-
+ +
+ -
+ {hasDrafts && ( - - {count} MODIF. - + <> +
+ + {count} MODIF. + + )}