- 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>
201 lines
6.4 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|