Reverting the production revert so the pm branch can merge cleanly on top of the restored RBAC base. Conflicts against newer integration work resolved by combining: - App.tsx: v7's bot-draft wizard bypass + login returnUrl kept alongside the supervisor gating (no-project block screen, panel scoping) - LoginPage: returnUrl navigation kept, supervisor routing added - migration.sql: bot_drafts table kept, unified-projects ALTERs restored
167 lines
6.8 KiB
TypeScript
167 lines
6.8 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { Link } from "react-router-dom";
|
|
import { ChevronDown, Menu, X } from "lucide-react";
|
|
import { cn } from "../lib/utils";
|
|
|
|
export interface MenuItem {
|
|
label?: string;
|
|
to?: string;
|
|
// لینک خارجی (اپ جداگانه، مثل سامانه PM) — در تب جدید باز میشود
|
|
href?: string;
|
|
active?: boolean;
|
|
divider?: boolean;
|
|
onClick?: () => void;
|
|
// آیتم بازشو (آکاردئونی) — مثلاً فهرست پروژههای مدیر پروژه زیر «مدیریت انبار»
|
|
children?: MenuItem[];
|
|
}
|
|
|
|
// منوی همبرگری — بالای راست صفحات؛ نمایش پنل فعلی + ناوبری بین پنلها
|
|
export function PanelMenu({ items, title }: { items: MenuItem[]; title?: string }) {
|
|
const [open, setOpen] = useState(false);
|
|
const [expanded, setExpanded] = useState<number | null>(null);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onDoc = (e: MouseEvent) => {
|
|
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
|
};
|
|
document.addEventListener("mousedown", onDoc);
|
|
return () => document.removeEventListener("mousedown", onDoc);
|
|
}, [open]);
|
|
|
|
return (
|
|
<div ref={ref} className="relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen((o) => !o)}
|
|
className="inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-lg border border-line bg-panel text-ink hover:bg-slate-bg"
|
|
aria-label="منو"
|
|
title="منو"
|
|
>
|
|
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
</button>
|
|
{open && (
|
|
<div className="absolute top-11 right-0 z-50 min-w-56 rounded-xl border border-line bg-panel p-1.5 shadow-lg">
|
|
{title && <div className="px-3 py-1.5 text-[11px] font-bold text-muted">{title}</div>}
|
|
{items.map((it, i) =>
|
|
it.divider ? (
|
|
<div key={i} className="my-1 border-t border-line" />
|
|
) : it.children?.length ? (
|
|
<div key={i}>
|
|
<div className={cn("flex items-center rounded-lg", it.active && "bg-accent")}>
|
|
{it.to ? (
|
|
<Link
|
|
to={it.to}
|
|
onClick={() => {
|
|
setOpen(false);
|
|
it.onClick?.();
|
|
}}
|
|
className={cn(
|
|
"flex flex-1 items-center justify-between gap-3 rounded-lg px-3 py-2 text-[13px] no-underline",
|
|
it.active ? "font-bold text-white" : "text-ink hover:bg-slate-bg",
|
|
)}
|
|
>
|
|
{it.label}
|
|
{it.active && <span className="text-[10px] opacity-80">✓ فعلی</span>}
|
|
</Link>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setOpen(false);
|
|
it.onClick?.();
|
|
}}
|
|
className="flex flex-1 items-center gap-3 rounded-lg px-3 py-2 text-start text-[13px] text-ink hover:bg-slate-bg"
|
|
>
|
|
{it.label}
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
aria-label={expanded === i ? "بستن فهرست پروژهها" : "نمایش پروژهها"}
|
|
aria-expanded={expanded === i}
|
|
onClick={() => setExpanded(expanded === i ? null : i)}
|
|
className={cn(
|
|
"me-1 inline-flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-md",
|
|
it.active ? "text-white hover:bg-white/15" : "text-muted hover:bg-slate-bg",
|
|
)}
|
|
>
|
|
<ChevronDown
|
|
className={cn("h-4 w-4 transition-transform", expanded === i && "rotate-180")}
|
|
/>
|
|
</button>
|
|
</div>
|
|
{expanded === i && (
|
|
<div className="mt-0.5 flex flex-col gap-0.5 border-s border-line ps-2 ms-3">
|
|
{it.children.map((c, j) => (
|
|
<Link
|
|
key={j}
|
|
to={c.to ?? "#"}
|
|
onClick={() => {
|
|
setOpen(false);
|
|
c.onClick?.();
|
|
}}
|
|
className={cn(
|
|
"flex items-center justify-between gap-2 rounded-lg px-2.5 py-1.5 text-[12.5px] no-underline",
|
|
c.active ? "bg-accent-bg font-bold text-accent" : "text-ink hover:bg-slate-bg",
|
|
)}
|
|
>
|
|
<span className="truncate">{c.label}</span>
|
|
{c.active && <span className="shrink-0 text-[10px]">✓ فعلی</span>}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : it.href ? (
|
|
<a
|
|
key={i}
|
|
href={it.href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={() => setOpen(false)}
|
|
className={cn(
|
|
"flex items-center justify-between gap-3 rounded-lg px-3 py-2 text-[13px] no-underline",
|
|
it.active ? "bg-accent font-bold text-white" : "text-ink hover:bg-slate-bg",
|
|
)}
|
|
>
|
|
{it.label}
|
|
<span aria-hidden className="text-[10px] opacity-60">↗</span>
|
|
</a>
|
|
) : it.to ? (
|
|
<Link
|
|
key={i}
|
|
to={it.to}
|
|
onClick={() => {
|
|
setOpen(false);
|
|
it.onClick?.();
|
|
}}
|
|
className={cn(
|
|
"flex items-center justify-between gap-3 rounded-lg px-3 py-2 text-[13px] no-underline",
|
|
it.active ? "bg-accent font-bold text-white" : "text-ink hover:bg-slate-bg",
|
|
)}
|
|
>
|
|
{it.label}
|
|
{it.active && <span className="text-[10px] opacity-80">✓ فعلی</span>}
|
|
</Link>
|
|
) : (
|
|
<button
|
|
key={i}
|
|
type="button"
|
|
onClick={() => {
|
|
setOpen(false);
|
|
it.onClick?.();
|
|
}}
|
|
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-start text-[13px] text-ink hover:bg-slate-bg"
|
|
>
|
|
{it.label}
|
|
</button>
|
|
),
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|