mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat: Implement dashboard layout, shared sidebar, AI copilot analysis and scoring engines, and a grid floating summary bar.
This commit is contained in:
1 parent
a092572829
commit
9d8ef0e4db
5 files changed
+100
-27
No files matched your search
@@ -5,7 +5,7 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden" style={{ background: "var(--bg-base)" }}>
|
||||
<Sidebar />
|
||||
<main className="flex-1 ml-16 flex flex-col h-full min-w-0 overflow-hidden">
|
||||
<main className="flex-1 flex flex-col h-full min-w-0 overflow-hidden">
|
||||
<Header />
|
||||
<div className="flex-1 overflow-auto p-4 md:p-8">
|
||||
{children}
|
||||
|
||||
@@ -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 (
|
||||
<aside
|
||||
className="w-16 flex-shrink-0 flex flex-col items-center h-screen fixed left-0 top-0 z-30 glass"
|
||||
className={cn(
|
||||
"flex-shrink-0 flex flex-col h-screen z-30 glass transition-all duration-300 relative",
|
||||
isExpanded ? "w-60" : "w-16 items-center"
|
||||
)}
|
||||
style={{ borderRight: "1px solid var(--border)" }}
|
||||
>
|
||||
{/* Toggle Button */}
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="absolute -right-3 top-6 w-6 h-6 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-full flex items-center justify-center shadow-sm text-slate-500 hover:text-slate-800 dark:hover:text-slate-300 transition-colors z-40"
|
||||
>
|
||||
{isExpanded ? <ChevronLeft className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="py-[18px] w-full flex justify-center" style={{ borderBottom: "1px solid var(--border)" }}>
|
||||
<div className="w-8 h-8 rounded-xl bg-[var(--accent)] flex items-center justify-center shadow-sm border border-white/10" title="CollectFlow">
|
||||
<Package className="w-[18px] h-[18px] text-white" strokeWidth={2.2} />
|
||||
<div className={cn("py-[18px] w-full flex", isExpanded ? "px-5" : "justify-center")} style={{ borderBottom: "1px solid var(--border)" }}>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-8 h-8 rounded-xl bg-[var(--accent)] flex items-center justify-center shrink-0 shadow-sm border border-white/10" title="CollectFlow">
|
||||
<Package className="w-[18px] h-[18px] text-white" strokeWidth={2.2} />
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<span className="text-[15px] font-semibold tracking-[-0.3px] text-[var(--text-primary)] whitespace-nowrap overflow-hidden">
|
||||
CollectFlow
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="w-full flex-1 flex flex-col items-center py-4 space-y-2">
|
||||
<nav className={cn("w-full flex-1 flex flex-col py-4 space-y-2", isExpanded ? "px-2" : "items-center px-0")}>
|
||||
{filteredItems.map(({ icon: Icon, label, href }) => {
|
||||
const isActive = pathname.startsWith(href);
|
||||
|
||||
@@ -56,26 +84,55 @@ export function Sidebar() {
|
||||
<Link
|
||||
key={href}
|
||||
href={resolvedHref}
|
||||
title={label}
|
||||
title={!isExpanded ? label : undefined}
|
||||
style={{
|
||||
color: isActive ? "var(--text-primary)" : "var(--text-secondary)",
|
||||
background: isActive ? "var(--bg-elevated)" : "transparent",
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-center w-10 h-10 rounded-xl transition-all duration-150",
|
||||
"flex items-center rounded-xl transition-all duration-150 overflow-hidden",
|
||||
isExpanded ? "px-3 py-[7px] gap-2.5 w-full" : "justify-center w-10 h-10",
|
||||
!isActive && "hover:bg-[var(--bg-elevated)]"
|
||||
)}
|
||||
>
|
||||
<Icon className="w-[18px] h-[18px] shrink-0" strokeWidth={1.8} />
|
||||
<Icon className={cn("shrink-0", isExpanded ? "w-[15px] h-[15px]" : "w-[18px] h-[18px]")} strokeWidth={1.8} />
|
||||
{isExpanded && <span className="text-[13px] font-medium whitespace-nowrap">{label}</span>}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* User Profile & Logout */}
|
||||
<div className="w-full pb-4 flex flex-col items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-[var(--bg-elevated)] border border-[var(--border)] flex items-center justify-center shadow-sm" title={session?.user?.name || "Profil"}>
|
||||
<UserIcon className="w-5 h-5 text-[var(--accent)]" />
|
||||
<div className={cn("w-full pb-4 flex flex-col gap-3", isExpanded ? "px-3" : "items-center")}>
|
||||
<div className={cn(
|
||||
"rounded-xl bg-[var(--bg-elevated)] border border-[var(--border)] flex items-center shadow-sm overflow-hidden",
|
||||
isExpanded ? "px-3 py-3 gap-3 w-full" : "justify-center w-10 h-10"
|
||||
)}
|
||||
title={!isExpanded ? (session?.user?.name || "Profil") : undefined}
|
||||
>
|
||||
<div className={cn(
|
||||
"rounded-lg bg-[var(--accent-bg)] flex items-center justify-center border border-[var(--accent-border)] shrink-0",
|
||||
isExpanded ? "w-8 h-8" : "w-6 h-6 border-transparent bg-transparent"
|
||||
)}>
|
||||
<UserIcon className={cn("text-[var(--accent)]", isExpanded ? "w-4 h-4" : "w-5 h-5")} />
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div className="flex-1 min-w-0">
|
||||
{session?.user ? (
|
||||
<>
|
||||
<p className="text-[12px] font-bold text-[var(--text-primary)] truncate">{session.user.name}</p>
|
||||
<p className="text-[10px] uppercase font-black text-[var(--text-muted)] tracking-wider">
|
||||
{userRole === "admin" ? "Administrateur" : "Utilisateur"}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="w-3 h-3 animate-spin text-[var(--accent)]" />
|
||||
<p className="text-[10px] text-[var(--text-muted)] animate-pulse">Session...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -83,10 +140,14 @@ export function Sidebar() {
|
||||
await signOut({ redirect: false });
|
||||
window.location.href = "/login";
|
||||
}}
|
||||
title="Déconnexion"
|
||||
className="w-10 h-10 flex items-center justify-center rounded-xl text-[var(--accent-error)] hover:bg-[var(--accent-error-bg)] transition-colors"
|
||||
title={!isExpanded ? "Déconnexion" : undefined}
|
||||
className={cn(
|
||||
"flex items-center text-[var(--accent-error)] hover:bg-[var(--accent-error-bg)] transition-colors rounded-xl overflow-hidden",
|
||||
isExpanded ? "w-full gap-2.5 px-3 py-2 text-[13px] font-medium" : "justify-center w-10 h-10"
|
||||
)}
|
||||
>
|
||||
<LogOut className="w-[18px] h-[18px]" strokeWidth={1.8} />
|
||||
<LogOut className={cn("shrink-0", isExpanded ? "w-[15px] h-[15px]" : "w-[18px] h-[18px]")} strokeWidth={1.8} />
|
||||
{isExpanded && "Déconnexion"}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -114,28 +114,33 @@ export function FloatingSummaryBar() {
|
||||
borderColor: hasDrafts ? "rgba(234, 179, 8, 0.5)" : "var(--border-strong)",
|
||||
}}
|
||||
>
|
||||
<div className="text-xs font-bold px-4 py-2 rounded-full border flex items-center bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 text-slate-500">
|
||||
<div className="flex items-center bg-white dark:bg-slate-900 shadow-sm border border-slate-200 dark:border-slate-800 rounded-full px-1.5 py-1.5 h-10">
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="p-1.5 hover:bg-rose-50 dark:hover:bg-rose-900/20 text-slate-400 hover:text-rose-500 transition-colors rounded-lg border border-transparent hover:border-rose-200"
|
||||
className="w-7 h-7 flex items-center justify-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors rounded-full"
|
||||
title="Réinitialiser la page"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="w-[1px] h-4 bg-slate-200 dark:bg-slate-800 mx-1.5" />
|
||||
|
||||
<div className="w-px h-4 bg-slate-200 dark:bg-slate-700 mx-1" />
|
||||
|
||||
<button
|
||||
onClick={() => handleSnapshot()}
|
||||
disabled={isSavingSnapshot}
|
||||
className="p-1.5 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 text-slate-400 hover:text-indigo-500 transition-colors rounded-lg border border-transparent hover:border-indigo-200 disabled:opacity-50"
|
||||
className="w-7 h-7 flex items-center justify-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors rounded-full disabled:opacity-50"
|
||||
title="Prendre un snapshot de la session"
|
||||
>
|
||||
{isSavingSnapshot ? <Loader2 className="w-4 h-4 animate-spin" /> : <Camera className="w-4 h-4" />}
|
||||
</button>
|
||||
<div className="w-[1px] h-4 bg-slate-200 dark:bg-slate-800 mx-1.5" />
|
||||
|
||||
{hasDrafts && (
|
||||
<span className="text-[12px] font-black tracking-tight text-[#b45309] bg-[#fef08a] px-3 py-1 rounded-md border border-[#fde047] ml-2">
|
||||
{count} MODIF.
|
||||
</span>
|
||||
<>
|
||||
<div className="w-px h-4 bg-slate-200 dark:bg-slate-700 mx-1" />
|
||||
<span className="ml-1 px-3 h-7 flex items-center justify-center rounded-full text-[11px] font-black tracking-wide text-[#b45309] dark:text-[#78350f] bg-[#fdf08a] hover:bg-[#fde047] transition-colors border border-[#fde047]/50 shadow-sm uppercase">
|
||||
{count} MODIF.
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user