Sidebar.tsx 11.4 KB
import React, { useMemo, useState } from 'react';
import { 
  LayoutDashboard, 
  Tag, 
  MapPin, 
  Users, 
  Package, 
  FileText, 
  HelpCircle, 
  LogOut, 
  ChevronDown, 
  ChevronRight,
  Layers,
  Type,
  FileBox,
  Settings
} from 'lucide-react';
import { cn } from '../ui/utils';
import { ScrollArea } from '../ui/scroll-area';
import logo from "figma:asset/773f0c39e1986271e9144596caac519f934a6ae6.png";
import type { CurrentUserMenuNodeDto } from '../../types/authSession';

interface SidebarProps {
  currentView: string;
  setCurrentView: (view: string) => void;
  menus?: CurrentUserMenuNodeDto[];
  onLogout?: () => void;
}

const FALLBACK_ICONS = {
  Dashboard: LayoutDashboard,
  Labeling: Tag,
  Labels: Tag,
  "Label Categories": Layers,
  "Label Types": Type,
  "Label Templates": FileBox,
  "Multiple Option Sets": Settings,
  "Multiple Options": Settings,
  "Location Manager": MapPin,
  "Account Management": Users,
  "Menu Management": Package,
  "System Menu": Settings,
  Reports: FileText,
  Support: HelpCircle,
  "Log Out": LogOut,
} as const;

function normalizeLabel(name: string) {
  return name.trim() || "N/A";
}

function displayMenuLabel(name: string) {
  const s = normalizeLabel(name);
  if (s === "Multiple Options") return "Multiple Option Sets";
  return s;
}

function pickIconKeyFromNode(node: CurrentUserMenuNodeDto, label: string): keyof typeof FALLBACK_ICONS | null {
  const raw = String((node as any)?.menuIcon ?? (node as any)?.MenuIcon ?? "").trim();
  if (raw && raw in FALLBACK_ICONS) return raw as keyof typeof FALLBACK_ICONS;
  if (label in FALLBACK_ICONS) return label as keyof typeof FALLBACK_ICONS;
  return null;
}

function MenuNode({
  node,
  level,
  currentKey,
  onSelect,
}: {
  node: CurrentUserMenuNodeDto;
  level: number;
  currentKey: string;
  onSelect: (key: string) => void;
}) {
  const [open, setOpen] = React.useState(true);
  const children = node.children ?? [];
  const isDir = (node.menuType ?? 0) === 0 || children.length > 0;
  const label = normalizeLabel(String(node.menuName ?? node.routerName ?? node.routeUrl ?? node.id ?? ""));
  const viewKey = label === "Multiple Options" ? "Multiple Option Sets" : label;
  const active = currentKey === viewKey;
  const iconKey = pickIconKeyFromNode(node, viewKey);
  const Icon = iconKey ? FALLBACK_ICONS[iconKey] : null;

  if (isDir) {
    return (
      <div className="space-y-1">
        <button
          onClick={() => setOpen((x) => !x)}
          className={cn(
            "w-full flex items-center justify-between px-4 py-2.5 text-sm font-medium rounded-lg transition-colors",
            "hover:bg-blue-800/50 text-blue-100",
            level > 0 && "ml-1",
          )}
          style={{ paddingLeft: 16 + level * 12 }}
        >
          <div className="flex items-center gap-3 min-w-0">
            {level === 0 && Icon ? <Icon className="w-4 h-4 shrink-0" /> : <div className="w-1.5 h-1.5 rounded-full bg-blue-200 shrink-0" />}
            <span className="truncate">{displayMenuLabel(label)}</span>
          </div>
          {open ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
        </button>
        {open && children.length > 0 && (
          <div className="space-y-1">
            {children.map((c) => (
              <MenuNode key={String(c.id ?? Math.random())} node={c} level={level + 1} currentKey={currentKey} onSelect={onSelect} />
            ))}
          </div>
        )}
      </div>
    );
  }

  return (
    <button
      onClick={() => onSelect(label)}
      className={cn(
        "w-full flex items-center gap-3 px-4 py-2.5 text-sm font-medium rounded-lg transition-colors",
        active ? "bg-blue-700 text-white shadow-md shadow-blue-900/20" : "text-blue-100 hover:bg-blue-800 hover:text-white",
      )}
      style={{ paddingLeft: 16 + level * 12 }}
    >
      {level === 0 && Icon ? <Icon className="w-4 h-4 shrink-0" /> : <div className="w-1 h-1 rounded-full bg-current" />}
      <span className="truncate">{displayMenuLabel(label)}</span>
    </button>
  );
}

export function Sidebar({ currentView, setCurrentView, menus, onLogout }: SidebarProps) {
  const [labelingOpen, setLabelingOpen] = useState(true);

  const fallbackMenuItems = [
    { name: 'Dashboard', icon: LayoutDashboard, type: 'item' },
    { type: 'header', name: 'MODULES' },
    { 
      name: 'Labeling', 
      icon: Tag, 
      type: 'sub', 
      isOpen: labelingOpen, 
      toggle: () => setLabelingOpen(!labelingOpen),
      children: [
        { name: 'Labels', icon: Tag },
        { name: 'Label Categories', icon: Layers },
        { name: 'Label Types', icon: Type },
        { name: 'Label Templates', icon: FileBox },
        { name: 'Multiple Option Sets', icon: Settings },
      ]
    },
    { type: 'header', name: 'MANAGEMENT' },
    { name: 'Account Management', icon: Users, type: 'item' },
    { name: 'Menu Management', icon: Package, type: 'item' },
    { name: 'System Menu', icon: Settings, type: 'item' },
    { name: 'Reports', icon: FileText, type: 'item' },
    { name: 'Support', icon: HelpCircle, type: 'item' },
    { name: 'Log Out', icon: LogOut, type: 'item' },
  ];

  const hasRemoteMenus = (menus?.length ?? 0) > 0;
  const logoutHandler = () => (onLogout ? onLogout() : setCurrentView("Log Out"));

  const remoteMenuNodes = useMemo(() => menus ?? [], [menus]);
  const { moduleMenus, accountMenus } = useMemo(() => {
    const ACCOUNT_LABELS = new Set([
      "Location Manager",
      "Account Management",
      "Menu Management",
      "System Menu",
      "Reports",
      "Support",
    ]);

    const roots = remoteMenuNodes;
    const moduleMenus: CurrentUserMenuNodeDto[] = [];
    const accountMenus: CurrentUserMenuNodeDto[] = [];

    for (const n of roots) {
      const label = normalizeLabel(String(n.menuName ?? n.routerName ?? n.routeUrl ?? n.id ?? ""));
      if (label === "Management") {
        const children = n.children ?? [];
        for (const c of children) {
          const cl = normalizeLabel(String(c.menuName ?? c.routerName ?? c.routeUrl ?? c.id ?? ""));
          if (ACCOUNT_LABELS.has(cl)) accountMenus.push(c);
          else moduleMenus.push(c);
        }
        continue;
      }

      if (ACCOUNT_LABELS.has(label)) {
        accountMenus.push(n);
        continue;
      }
      moduleMenus.push(n);
    }

    return { moduleMenus, accountMenus };
  }, [remoteMenuNodes]);

  return (
    <div className="w-64 bg-[#1e3a8a] text-white flex flex-col h-screen border-r border-blue-800 shadow-xl z-20 shrink-0">
      <div className="flex items-center justify-center border-b border-blue-800/50 bg-white px-4 shrink-0" style={{ height: 90 }}>
        <img src={logo} alt="MedVantage" className="h-16 w-auto object-contain" />
      </div>

      <ScrollArea className="flex-1 py-4">
        <div className="px-3 space-y-1">
          {hasRemoteMenus ? (
            <>
              <button
                onClick={() => setCurrentView("Dashboard")}
                className={cn(
                  "w-full flex items-center gap-3 px-4 py-2.5 text-sm font-medium rounded-lg transition-colors",
                  currentView === "Dashboard" ? "bg-blue-700 text-white shadow-md shadow-blue-900/20" : "text-blue-100 hover:bg-blue-800 hover:text-white",
                )}
              >
                <LayoutDashboard className="w-4 h-4" />
                Dashboard
              </button>

              <div className="px-4 py-2 mt-4 text-xs font-semibold text-blue-300 uppercase tracking-wider">MODULES</div>
              {moduleMenus.map((n) => (
                <MenuNode key={String(n.id ?? Math.random())} node={n} level={0} currentKey={currentView} onSelect={setCurrentView} />
              ))}

              <div className="px-4 py-2 mt-4 text-xs font-semibold text-blue-300 uppercase tracking-wider">MANAGEMENT</div>
              {accountMenus.map((n) => (
                <MenuNode key={String(n.id ?? Math.random())} node={n} level={0} currentKey={currentView} onSelect={setCurrentView} />
              ))}
              <button
                onClick={logoutHandler}
                className={cn(
                  "w-full flex items-center gap-3 px-4 py-2.5 text-sm font-medium rounded-lg transition-colors",
                  "text-red-300 hover:bg-red-900/20 hover:text-red-200",
                )}
              >
                <LogOut className="w-4 h-4" />
                Log Out
              </button>
            </>
          ) : (
            fallbackMenuItems.map((item, index) => {
            if (item.type === 'header') {
              return (
                <div key={index} className="px-4 py-2 mt-4 text-xs font-semibold text-blue-300 uppercase tracking-wider">
                  {item.name}
                </div>
              );
            }

            if (item.type === 'sub') {
              const Icon = item.icon;
              return (
                <div key={index} className="space-y-1">
                  <button
                    onClick={item.toggle}
                    className={cn(
                      "w-full flex items-center justify-between px-4 py-2.5 text-sm font-medium rounded-lg transition-colors",
                      "hover:bg-blue-800/50 text-blue-100"
                    )}
                  >
                    <div className="flex items-center gap-3">
                      {Icon ? <Icon className="w-4 h-4" /> : null}
                      {item.name}
                    </div>
                    {item.isOpen ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
                  </button>
                  
                  {item.isOpen && (
                    <div className="pl-4 space-y-1">
                      {item.children?.map((child, childIndex) => (
                        <button
                          key={childIndex}
                          onClick={() => setCurrentView(child.name)}
                          className={cn(
                            "w-full flex items-center gap-3 px-4 py-2 text-sm font-medium rounded-lg transition-colors border-l-2",
                            currentView === child.name
                              ? "bg-blue-800 border-blue-400 text-white"
                              : "border-transparent hover:bg-blue-800/30 text-blue-200 hover:text-white"
                          )}
                        >
                          <div className="w-1 h-1 rounded-full bg-current" />
                          {child.name}
                        </button>
                      ))}
                    </div>
                  )}
                </div>
              );
            }

            const Icon = item.icon;
            return (
              <button
                key={index}
                onClick={() => (item.name === "Log Out" ? logoutHandler() : setCurrentView(item.name))}
                className={cn(
                  "w-full flex items-center gap-3 px-4 py-2.5 text-sm font-medium rounded-lg transition-colors",
                  currentView === item.name
                    ? "bg-blue-700 text-white shadow-md shadow-blue-900/20"
                    : item.name === 'Log Out'
                      ? "text-red-300 hover:bg-red-900/20 hover:text-red-200"
                      : "text-blue-100 hover:bg-blue-800 hover:text-white"
                )}
              >
                {Icon ? <Icon className="w-4 h-4" /> : null}
                {item.name}
              </button>
            );
          }))}
        </div>
      </ScrollArea>
    </div>
  );
}