amazon-transcreation/frontend/src/components/layout/Sidebar.tsx
Vadym Samoilenko 075bbb69e5 Implement user management: viewer role, real API wiring, admin sidebar
- Add viewer role to backend enum + Alembic migration
- SSO auto-provisioned users now get viewer (lowest privilege) by default
- Wire admin/users page to real API (replace mock data), with add/edit/deactivate
- Fix frontend UserRole enum to match backend (TM_MANAGER, REVIEWER)
- Replace hardcoded mock user in Sidebar with real auth, filter admin-only nav items, wire logout
- Add seed script to set default admins (daveporter, vadymsamoilenko)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 18:37:16 +01:00

201 lines
6.4 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import Image from "next/image";
import { usePathname, useSearchParams, useRouter } from "next/navigation";
import {
LayoutDashboard,
PlusCircle,
Activity,
Database,
BookOpen,
Building2,
BarChart3,
ScrollText,
Users,
Terminal,
HelpCircle,
LogOut,
Menu,
X,
} from "lucide-react";
import { cn, getInitials } from "@/lib/utils";
import { getUser, clearAuth, getRoleDisplayName } from "@/lib/auth";
interface NavItem {
label: string;
href: string;
icon: React.ElementType;
adminOnly?: boolean;
}
const navItems: NavItem[] = [
{ label: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
{ label: "New Job", href: "/jobs/new", icon: PlusCircle },
{ label: "Monitoring", href: "/dashboard?view=monitoring", icon: Activity },
{ label: "TM Registry", href: "/admin/files/tm", icon: Database },
{ label: "Reference Library", href: "/admin/files/reference", icon: BookOpen },
{ label: "Clients & Voice", href: "/admin/clients", icon: Building2 },
{ label: "Analytics", href: "/admin/reports", icon: BarChart3 },
{ label: "Audit Trail", href: "/admin/logs", icon: ScrollText },
{ label: "User Management", href: "/admin/users", icon: Users, adminOnly: true },
{ label: "System Logs", href: "/admin/logs?type=system", icon: Terminal, adminOnly: true },
{ label: "Help", href: "/help", icon: HelpCircle },
];
export function Sidebar() {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
const [mobileOpen, setMobileOpen] = useState(false);
const [currentUser, setCurrentUser] = useState<{
name: string;
role: string;
email: string;
} | null>(null);
useEffect(() => {
const stored = getUser();
if (stored) {
setCurrentUser({ name: stored.name, role: stored.role, email: stored.email });
}
}, []);
const handleLogout = () => {
clearAuth();
router.push("/login");
};
const isAdmin = currentUser?.role === "ADMIN";
const visibleNavItems = navItems.filter(
(item) => !item.adminOnly || isAdmin
);
const isActive = (href: string) => {
const [hrefPath, hrefQuery] = href.split("?");
const basePath = hrefPath;
if (basePath === "/dashboard") {
const hrefHasView = hrefQuery?.includes("view=monitoring");
const currentHasView = searchParams.get("view") === "monitoring";
if (hrefHasView) return pathname === "/dashboard" && currentHasView;
return pathname === "/dashboard" && !currentHasView;
}
if (basePath === "/admin/logs") {
const hrefHasSystem = hrefQuery?.includes("type=system");
const currentHasSystem = searchParams.get("type") === "system";
if (hrefHasSystem) return pathname === "/admin/logs" && currentHasSystem;
return pathname === "/admin/logs" && !currentHasSystem;
}
return pathname.startsWith(basePath);
};
const sidebarContent = (
<div className="flex flex-col h-full">
{/* Logo */}
<div className="p-4 pb-2">
<div className="bg-white rounded-lg p-3 flex items-center justify-center">
<Image
src={`${process.env.NEXT_PUBLIC_BASE_PATH || ""}/amazon-logo.svg`}
alt="Amazon"
width={210}
height={70}
className="object-contain"
priority
/>
</div>
<p className="text-center text-xs text-gray-400 mt-2 font-medium tracking-wide uppercase">
Transcreation Platform
</p>
</div>
{/* Separator */}
<div className="mx-4 my-2 h-px bg-white/10" />
{/* Navigation */}
<nav className="flex-1 px-3 py-2 space-y-0.5 overflow-y-auto">
{visibleNavItems.map((item) => {
const Icon = item.icon;
const active = isActive(item.href);
return (
<Link
key={item.href}
href={item.href}
onClick={() => setMobileOpen(false)}
className={cn(
"flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-all duration-150 relative group",
active
? "text-amazon-orange bg-white/10"
: "text-gray-300 hover:text-white hover:bg-white/5"
)}
>
{active && (
<span className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-5 bg-amazon-orange rounded-r-full" />
)}
<Icon className={cn("h-[18px] w-[18px] shrink-0", active ? "text-amazon-orange" : "text-gray-400 group-hover:text-gray-300")} />
<span className="font-medium">{item.label}</span>
</Link>
);
})}
</nav>
{/* User section */}
<div className="p-3 border-t border-white/10">
<div className="flex items-center gap-3 px-3 py-2">
<div className="h-9 w-9 rounded-full bg-amazon-orange flex items-center justify-center text-white text-sm font-bold shrink-0">
{getInitials(currentUser?.name || "U")}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white truncate">
{currentUser?.name || "User"}
</p>
<p className="text-xs text-gray-400 truncate">
{currentUser ? getRoleDisplayName(currentUser.role) : ""}
</p>
</div>
<button
className="text-gray-400 hover:text-white transition-colors p-1"
title="Sign out"
onClick={handleLogout}
>
<LogOut className="h-4 w-4" />
</button>
</div>
</div>
</div>
);
return (
<>
{/* Mobile toggle */}
<button
className="lg:hidden fixed top-4 left-4 z-50 p-2 rounded-lg bg-amazon-dark text-white shadow-lg"
onClick={() => setMobileOpen(!mobileOpen)}
>
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
{/* Mobile overlay */}
{mobileOpen && (
<div
className="lg:hidden fixed inset-0 bg-black/50 z-40"
onClick={() => setMobileOpen(false)}
/>
)}
{/* Sidebar */}
<aside
className={cn(
"fixed top-0 left-0 h-screen w-[260px] bg-amazon-dark z-40 transition-transform duration-300 lg:translate-x-0",
mobileOpen ? "translate-x-0" : "-translate-x-full"
)}
>
{sidebarContent}
</aside>
</>
);
}