- /panel/pm: the PM app (served on the same origin under /pm) is embedded as a full-height iframe; supervisors are funneled there from every route, ERP MobileTabBar hidden on that route - Panels menu: «مدیریت پروژه (PM)» is an internal route (admin/pm/supervisor only — procurement managers do not see it); PM link is no longer external - New system role 'supervisor' (user_role enum): label in users router, allowed in UserRoleUpdate schema, deny-by-default on all ERP routers; admin panel can grant/revoke it alongside procurement manager - Removed the obsolete VITE_PM_URL build plumbing (same-origin now)
219 lines
9.7 KiB
TypeScript
219 lines
9.7 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { ShieldCheck, Warehouse as WarehouseIcon } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { Header, PageLayout } from "../../components/layout/Header";
|
|
import { EmptyState } from "../../components/EmptyState";
|
|
import { useAuthStore } from "../../store/auth";
|
|
import { AppSwitcherMenu } from "../../components/AppSwitcherMenu";
|
|
import { inventorySubheader } from "../Inventory/panelMenu";
|
|
import { apiV26, type UserRow } from "../../lib/api";
|
|
import { refreshProcurementStore } from "../../hooks";
|
|
import { faNum } from "../../lib/formatters";
|
|
|
|
const ROLE_LABELS: Record<string, string> = {
|
|
pm: "مدیر پروژه",
|
|
procurement_manager: "مدیر تدارکات",
|
|
admin: "ادمین",
|
|
supervisor: "سرپرست پروژه",
|
|
};
|
|
|
|
// پنل مدیریت ادمین (فقط ادمین) — همگامسازی کاربران از Casdoor و
|
|
// اعطا/لغو نقش «مدیر تدارکات». تخصیص پروژه در این پنل نیست
|
|
// (مدیریت پروژهها در «سازماندهی» توسط مدیر تدارکات انجام میشود).
|
|
export function AdminPanel() {
|
|
const navigate = useNavigate();
|
|
const user = useAuthStore((s) => s.user);
|
|
|
|
const [users, setUsers] = useState<UserRow[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [syncing, setSyncing] = useState(false);
|
|
const [busyId, setBusyId] = useState<string | null>(null);
|
|
|
|
const isAdmin = user?.role === "admin";
|
|
|
|
const load = useCallback(async () => {
|
|
try {
|
|
setUsers(await apiV26.users.list());
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : "خطا در دریافت کاربران");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (isAdmin) void load();
|
|
}, [isAdmin, load]);
|
|
|
|
const sync = async () => {
|
|
setSyncing(true);
|
|
try {
|
|
const { users: synced, created } = await apiV26.users.sync();
|
|
setUsers(synced);
|
|
toast.success(
|
|
created > 0 ? `همگامسازی انجام شد — ${faNum(created)} کاربر جدید از Casdoor اضافه شد ✓` : "همگامسازی انجام شد — کاربر جدیدی یافت نشد",
|
|
);
|
|
refreshProcurementStore();
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : "خطا در همگامسازی با Casdoor");
|
|
} finally {
|
|
setSyncing(false);
|
|
}
|
|
};
|
|
|
|
const toggleProcurementManager = async (u: UserRow) => {
|
|
setBusyId(u.id);
|
|
const target = u.role === "procurement_manager" ? null : "procurement_manager";
|
|
try {
|
|
await apiV26.users.updateRole(u.id, target as "procurement_manager" | null);
|
|
toast.success(
|
|
target === "procurement_manager"
|
|
? `«${u.first_name} ${u.last_name}» مدیر تدارکات شد ✓`
|
|
: `نقش مدیر تدارکات از «${u.first_name} ${u.last_name}» گرفته شد`,
|
|
);
|
|
await load();
|
|
refreshProcurementStore();
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : "خطا در تغییر نقش");
|
|
} finally {
|
|
setBusyId(null);
|
|
}
|
|
};
|
|
|
|
// اعطا/لغو نقش «سرپرست پروژه» — کاربر فقطِ پنل مدیریت پروژه (PM)
|
|
const toggleSupervisor = async (u: UserRow) => {
|
|
setBusyId(u.id);
|
|
const target = u.role === "supervisor" ? null : "supervisor";
|
|
try {
|
|
await apiV26.users.updateRole(u.id, target as "supervisor" | null);
|
|
toast.success(
|
|
target === "supervisor"
|
|
? `«${u.first_name} ${u.last_name}» سرپرست پروژه شد ✓`
|
|
: `نقش سرپرست پروژه از «${u.first_name} ${u.last_name}» گرفته شد`,
|
|
);
|
|
await load();
|
|
refreshProcurementStore();
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : "خطا در تغییر نقش");
|
|
} finally {
|
|
setBusyId(null);
|
|
}
|
|
};
|
|
|
|
if (!isAdmin) {
|
|
return (
|
|
<div className="min-h-screen">
|
|
<Header title="پنل مدیریت ادمین" backTo="/inventory" backLabel="بازگشت به داشبورد انبار" />
|
|
<PageLayout>
|
|
<EmptyState emoji="🔒" text="این پنل فقط برای ادمین (سوپر یوزر) است." />
|
|
</PageLayout>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen">
|
|
<Header
|
|
title="پنل مدیریت ادمین"
|
|
sub={inventorySubheader(user)}
|
|
backTo="/inventory"
|
|
backLabel="بازگشت به داشبورد انبار"
|
|
icon={<ShieldCheck className="h-4.5 w-4.5" strokeWidth={2} />}
|
|
menu={<AppSwitcherMenu activeModule="warehouse" />}
|
|
actions={
|
|
<button
|
|
onClick={() => void sync()}
|
|
disabled={syncing}
|
|
className="btn-line-file cursor-pointer disabled:opacity-50"
|
|
>
|
|
{syncing ? "در حال همگامسازی…" : "🔄 همگامسازی از Casdoor"}
|
|
</button>
|
|
}
|
|
/>
|
|
<PageLayout className="flex flex-col gap-3">
|
|
<div className="rounded-lg border border-blue/20 bg-blue/5 px-3 py-2 text-[12px] leading-6 text-muted">
|
|
🛡️ وظیفهی اصلی ادمین، مدیریت مدیران تدارکات است: با دکمهی هر کاربر میتوانید نقش
|
|
«مدیر تدارکات» را اعطا یا لغو کنید. تخصیص پروژه به مدیران پروژه در بخش «سازماندهی» انجام میشود
|
|
(این پنل فقط نقشها را مدیریت میکند).
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="py-6 text-center text-[12.5px] text-muted">در حال بارگذاری کاربران…</div>
|
|
) : !users.length ? (
|
|
<EmptyState emoji="👥" text="کاربری یافت نشد — دکمهی «همگامسازی از Casdoor» را بزنید." />
|
|
) : (
|
|
<div className="overflow-hidden rounded-xl border border-line bg-panel">
|
|
<table className="w-full text-[12.5px]">
|
|
<thead>
|
|
<tr className="border-b border-line bg-panel text-right text-muted">
|
|
<th className="py-2 pe-3">نام کامل</th>
|
|
<th className="py-2 pe-3">شناسه Casdoor</th>
|
|
<th className="py-2 pe-3">نقش فعلی</th>
|
|
<th className="py-2">مدیریت نقشها</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{users.map((u) => {
|
|
const isPMgr = u.role === "procurement_manager";
|
|
const isSup = u.role === "supervisor";
|
|
return (
|
|
<tr key={u.id} className="border-b border-line/50">
|
|
<td className="py-2 pe-3 font-semibold">
|
|
{u.first_name} {u.last_name}
|
|
</td>
|
|
<td className="py-2 pe-3 text-muted ltr">{u.casdoor_id}</td>
|
|
<td className="py-2 pe-3">
|
|
<span className={`rounded-lg px-2 py-1 text-[11px] font-bold ${u.role ? "bg-accent-bg text-accent" : "bg-slate-bg text-muted"}`}>
|
|
{u.role ? ROLE_LABELS[u.role] : "بدون نقش"}
|
|
</span>
|
|
</td>
|
|
<td className="py-2">
|
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
<button
|
|
disabled={busyId === u.id}
|
|
onClick={() => void toggleProcurementManager(u)}
|
|
className={`rounded-lg border px-3 py-1 text-[11.5px] font-bold ${
|
|
isPMgr
|
|
? "border-red/30 bg-red/5 text-red hover:bg-red/10"
|
|
: "border-green/30 bg-green/5 text-green hover:bg-green/10"
|
|
} disabled:opacity-50`}
|
|
>
|
|
{busyId === u.id ? "…" : isPMgr ? "لغو نقش مدیر تدارکات" : "اعطای نقش مدیر تدارکات"}
|
|
</button>
|
|
<button
|
|
disabled={busyId === u.id}
|
|
onClick={() => void toggleSupervisor(u)}
|
|
className={`rounded-lg border px-3 py-1 text-[11.5px] font-bold ${
|
|
isSup
|
|
? "border-red/30 bg-red/5 text-red hover:bg-red/10"
|
|
: "border-green/30 bg-green/5 text-green hover:bg-green/10"
|
|
} disabled:opacity-50`}
|
|
>
|
|
{busyId === u.id ? "…" : isSup ? "لغو نقش سرپرست پروژه" : "اعطای نقش سرپرست پروژه"}
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
<div className="border-t border-line bg-panel px-4 py-2 text-[11px] text-muted">
|
|
{faNum(users.length)} کاربر — نقشها فقط توسط ادمین قابل تغییرند (RBAC).
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
onClick={() => navigate("/inventory")}
|
|
className="flex w-fit cursor-pointer items-center gap-1.5 rounded-lg border border-line bg-panel px-3 py-1.5 text-[12px] text-muted hover:bg-slate-bg hover:text-ink"
|
|
>
|
|
<WarehouseIcon className="h-3.5 w-3.5" />
|
|
بازگشت به داشبورد انبار
|
|
</button>
|
|
</PageLayout>
|
|
</div>
|
|
);
|
|
}
|