Revert 253c922: restore the PM/RBAC work to the integration
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
This commit is contained in:
@@ -1,48 +1,72 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
import { PanelMenu, type MenuItem } from "./PanelMenu";
|
||||
import { useLogout } from "../pages/Inventory/panelMenu";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
|
||||
// منوی همبرگری مشترک بین دو ماژول (پنل انبار / دفتر تدارکات) —
|
||||
// دقیقاً مثل منوی دفتر: عنوان «پنلها»، ماژول فعلی با استایل فعال (پر)
|
||||
// + «✓ فعلی»، ماژول دیگر بهعنوان سوییچر، و در پایان جداکننده + خروج.
|
||||
// مدیر پروژه (1:N): زیر «مدیریت انبار» یک آکاردئون باز میشود که همهی
|
||||
// پروژههای تخصیصیافته را نشان میدهد؛ پروژهی فعال با «✓ فعلی» مشخص است و
|
||||
// انتخاب هر پروژه فقط «پروژهی فعال» را عوض میکند (نشست/توکن دستنخورده).
|
||||
// منوی همبرگری مشترک بین ۴ پنل اصلی سامانه:
|
||||
// ۱. مدیریت انبار (/inventory)
|
||||
// ۲. مدیریت تدارکات (/)
|
||||
// ۳. مدیریت پروژه (/panel/pm)
|
||||
// ۴. سازماندهی (/organization)
|
||||
export function AppSwitcherMenu({
|
||||
activeModule,
|
||||
canSwitchToProcurement = true,
|
||||
}: {
|
||||
activeModule: "warehouse" | "procurement";
|
||||
canSwitchToProcurement?: boolean; // مدیر پروژه به دفتر تدارکات دسترسی ندارد
|
||||
activeModule: "warehouse" | "procurement" | "organization";
|
||||
canSwitchToProcurement?: boolean;
|
||||
}) {
|
||||
const logout = useLogout();
|
||||
const { pathname } = useLocation();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const setActiveProject = useAuthStore((s) => s.setActiveProject);
|
||||
const items: MenuItem[] = [];
|
||||
const isSupervisor = user?.role === "supervisor";
|
||||
|
||||
const isPm = user?.role === "pm";
|
||||
const pmProjects = isPm ? user?.projects ?? [] : [];
|
||||
const projectChildren: MenuItem[] | undefined =
|
||||
activeModule === "warehouse" && isPm
|
||||
? pmProjects.map((p) => ({
|
||||
label: p.name,
|
||||
to: pathname, // همان بخش فعلی؛ فقط پروژه عوض میشود
|
||||
active: p.id === user?.project_id,
|
||||
onClick: () => setActiveProject(p.id),
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
if (activeModule === "procurement") {
|
||||
items.push({ label: "مدیریت انبار", to: "/inventory" });
|
||||
items.push({ label: "مدیریت تدارکات", to: "/", active: true });
|
||||
} else {
|
||||
items.push({ label: "مدیریت انبار", to: "/inventory", active: true, children: projectChildren });
|
||||
if (canSwitchToProcurement) {
|
||||
items.push({ label: "مدیریت تدارکات", to: "/" });
|
||||
}
|
||||
// سرپرست پروژه: منحصراً دو پنل «انبار» و «مدیریت پروژه» نمایش داده میشود
|
||||
if (isSupervisor) {
|
||||
const supervisorItems: MenuItem[] = [
|
||||
{
|
||||
label: "انبار",
|
||||
to: "/inventory",
|
||||
active: activeModule === "warehouse",
|
||||
},
|
||||
{
|
||||
label: "مدیریت پروژه",
|
||||
to: "/panel/pm",
|
||||
},
|
||||
{ divider: true },
|
||||
{ label: "خروج از حساب", onClick: logout },
|
||||
];
|
||||
return <PanelMenu title="پنلها" items={supervisorItems} />;
|
||||
}
|
||||
|
||||
const canManage = user?.role === "admin" || user?.role === "procurement_manager" || user?.role === "pm";
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
label: "انبار",
|
||||
to: "/inventory",
|
||||
active: activeModule === "warehouse",
|
||||
},
|
||||
];
|
||||
|
||||
if (canSwitchToProcurement) {
|
||||
items.push({
|
||||
label: "تدارکات",
|
||||
to: "/",
|
||||
active: activeModule === "procurement",
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
label: "مدیریت پروژه",
|
||||
to: "/panel/pm",
|
||||
});
|
||||
|
||||
if (canManage) {
|
||||
items.push({
|
||||
label: "سازماندهی",
|
||||
to: "/organization",
|
||||
active: activeModule === "organization",
|
||||
});
|
||||
}
|
||||
|
||||
items.push({ divider: true });
|
||||
items.push({ label: "خروج از حساب", onClick: logout });
|
||||
return <PanelMenu title="پنلها" items={items} />;
|
||||
|
||||
@@ -6,6 +6,8 @@ import { cn } from "../lib/utils";
|
||||
export interface MenuItem {
|
||||
label?: string;
|
||||
to?: string;
|
||||
// لینک خارجی (اپ جداگانه، مثل سامانه PM) — در تب جدید باز میشود
|
||||
href?: string;
|
||||
active?: boolean;
|
||||
divider?: boolean;
|
||||
onClick?: () => void;
|
||||
@@ -112,6 +114,21 @@ export function PanelMenu({ items, title }: { items: MenuItem[]; title?: string
|
||||
</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}
|
||||
|
||||
@@ -16,11 +16,11 @@ export function RequirePmProjects({ children }: { children: ReactNode }) {
|
||||
const setProjects = useAuthStore((s) => s.setProjects);
|
||||
const logout = useLogout();
|
||||
|
||||
const isPm = user?.role === "pm";
|
||||
const [state, setState] = useState<"loading" | "ok" | "empty">(isPm ? "loading" : "ok");
|
||||
const isSupervisor = user?.role === "supervisor";
|
||||
const [state, setState] = useState<"loading" | "ok" | "empty">(isSupervisor ? "loading" : "ok");
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || user.role !== "pm") {
|
||||
if (!user || user.role !== "supervisor") {
|
||||
setState("ok");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,23 @@ import type { ReactNode } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { ChevronRight, ClipboardList } from "lucide-react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { useAuthStore, type AuthUser } from "../../store/auth";
|
||||
|
||||
const ROLE_LABELS: Record<string, string> = {
|
||||
admin: "ادمین",
|
||||
procurement_manager: "مدیر تدارکات",
|
||||
pm: "مدیر پروژه",
|
||||
supervisor: "سرپرست پروژه",
|
||||
};
|
||||
|
||||
export function userSubtitle(user: AuthUser | null | undefined): string {
|
||||
if (!user) return "";
|
||||
const fullName = `${user.first_name || ""} ${user.last_name || ""}`.trim() || user.display_name || "کاربر";
|
||||
const username = user.username || user.casdoor_id || "-";
|
||||
const role = user.role_label || (user.role ? ROLE_LABELS[user.role] : "بدون نقش");
|
||||
const proj = user.role === "supervisor" && user.project_name ? ` – پروژه: ${user.project_name}` : "";
|
||||
return `${fullName} | نام کاربری: ${username} | نقش: ${role}${proj}`;
|
||||
}
|
||||
|
||||
interface HeaderProps {
|
||||
title: string;
|
||||
@@ -18,6 +35,10 @@ interface HeaderProps {
|
||||
export function Header({ title, sub, backTo, backLabel = "بازگشت", actions, brand, icon, menu }: HeaderProps) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
|
||||
const userSub = userSubtitle(user);
|
||||
const displaySub = sub !== undefined ? sub : userSub;
|
||||
|
||||
// اگر کاربر از داخل برنامه به این صفحه آمده، یک قدم برمیگردیم؛
|
||||
// اگر مستقیم باز شده (بدون سابقهی داخلی)، به backTo میرویم.
|
||||
@@ -47,7 +68,7 @@ export function Header({ title, sub, backTo, backLabel = "بازگشت", actions
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-[17px] font-extrabold leading-6">{title}</h1>
|
||||
{sub && <div className="text-[11px] text-muted">{sub}</div>}
|
||||
{displaySub && <div className="text-[11.5px] text-muted font-normal mt-0.5">{displaySub}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ms-auto flex flex-wrap items-center gap-2">{actions}</div>
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
PackageSearch,
|
||||
Scale,
|
||||
Truck,
|
||||
UsersRound,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { cn } from "../../lib/utils";
|
||||
@@ -37,12 +36,12 @@ const PM_INV_TABS: TabDef[] = [
|
||||
{ to: "/inventory/arrivals", label: "کالاهای در راه", icon: PackageSearch },
|
||||
];
|
||||
|
||||
// تبهای پنل انبار برای ادمین / مدیر تدارکات (آینهی ناوبری بالای دسکتاپ)
|
||||
// تبهای پنل انبار برای ادمین / مدیر تدارکات / مدیر پروژه (آینهی ناوبری بالای دسکتاپ)
|
||||
const ADMIN_INV_TABS: TabDef[] = [
|
||||
{ to: "/inventory", label: "انبار", icon: Boxes, end: true },
|
||||
{ to: "/inventory/daily", label: "کنترل روزانه", icon: CalendarDays },
|
||||
{ to: "/inventory/warehouses", label: "انبارها", icon: Building2 },
|
||||
{ to: "/inventory/arrivals", label: "کالاهای در راه", icon: PackageSearch },
|
||||
{ to: "/inventory/org", label: "سازماندهی", icon: UsersRound },
|
||||
];
|
||||
|
||||
function TabBar({ tabs }: { tabs: TabDef[] }) {
|
||||
@@ -81,6 +80,10 @@ export function MobileTabBar() {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
if (pathname === "/login" || pathname === "/admin") return null;
|
||||
// پنل مدیریت پروژه: iframe تمامصفحهی PM است و خودش تببار داخلی دارد
|
||||
if (pathname === "/panel/pm") return null;
|
||||
// سرپرست پروژه فقطِ پنل PM است و هیچ تب ERP ندارد
|
||||
if (role === "supervisor") return null;
|
||||
const isPM = role === "pm";
|
||||
const isOffice = role === "admin" || role === "procurement_manager";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user