import { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; import { Header, PageLayout } from "../../components/layout/Header"; import { useAuthStore } from "../../store/auth"; import { inventorySubheader } from "./panelMenu"; import { apiV26, type UserRow } from "../../lib/api"; import { refreshProcurementStore } from "../../hooks"; import { useInventoryData } from "./useInventoryData"; import { faNum } from "../../lib/formatters"; import { ProjectMultiSelect } from "../../components/ProjectMultiSelect"; type Role = "pm" | "procurement_manager" | "admin" | null; const ROLE_LABELS: Record = { pm: "مدیر پروژه", procurement_manager: "مدیر تدارکات", admin: "ادمین", }; // سازمان‌دهی (فقط ادمین / مدیر تدارکات) — کاربران، مدیران پروژه و انبارهای هر پروژه export function OrganizationPage() { const navigate = useNavigate(); const user = useAuthStore((s) => s.user); const data = useInventoryData(); const [users, setUsers] = useState([]); const [loadingUsers, setLoadingUsers] = useState(true); const [syncing, setSyncing] = useState(false); const [busyId, setBusyId] = useState(null); const [newWh, setNewWh] = useState>({}); const [newWhDesc, setNewWhDesc] = useState>({}); const canManage = user?.role === "admin" || user?.role === "procurement_manager"; // فقط ادمین می‌تواند نقش‌ها را تغییر دهد/ارتقا دهد (RBAC) — مدیر تدارکات فقط // پروژه‌ی مدیران پروژه را تعیین/جابه‌جا می‌کند. const canChangeRoles = user?.role === "admin"; const loadUsers = useCallback(async () => { try { const list = await apiV26.users.list(); setUsers(list); } catch (err) { toast.error(err instanceof Error ? err.message : "خطا در دریافت کاربران"); } finally { setLoadingUsers(false); } }, []); useEffect(() => { if (canManage) void loadUsers(); }, [canManage, loadUsers]); const syncUsers = async () => { setSyncing(true); try { const { users: synced, created } = await apiV26.users.sync(); setUsers(synced); toast.success( created > 0 ? `همگام‌سازی انجام شد — ${faNum(created)} کاربر جدید از Casdoor اضافه شد ✓` : "همگام‌سازی انجام شد — کاربر جدیدی یافت نشد", ); await data.refresh(); } catch (err) { toast.error(err instanceof Error ? err.message : "خطا در همگام‌سازی با Casdoor"); } finally { setSyncing(false); } }; if (!canManage) { return (
این بخش فقط برای ادمین / مدیر تدارکات است.
); } // ---------- actions: users ---------- const changeRole = async (u: UserRow, role: Role) => { setBusyId(u.id); try { await apiV26.users.updateRole(u.id, role); toast.success(`نقش «${ROLE_LABELS[role ?? ""] ?? "بدون نقش"}» برای ${u.first_name} ${u.last_name} ثبت شد ✓`); await loadUsers(); refreshProcurementStore(); } catch (err) { toast.error(err instanceof Error ? err.message : "خطا در تغییر نقش"); } finally { setBusyId(null); } }; // پروژه‌های متصل به یک کاربر (1:N) — با پشتیبانی از شکل قدیمیِ تک‌پروژه‌ای const assignedProjectsOf = (u: UserRow) => u.projects ?? (u.project_id ? [{ id: u.project_id, name: u.project_name ?? "" }] : []); // همگام‌سازی چند-انتخابیِ پروژه‌ها: افزودن/حذف هر اختلاف با یک PATCH // (pm_user_id = کاربر برای اتصال، null برای جداسازی؛ مدیر قبلی جایگزین می‌شود) const syncUserProjects = async (u: UserRow, nextIds: string[]) => { const current = assignedProjectsOf(u).map((p) => p.id); const added = nextIds.filter((id) => !current.includes(id)); const removed = current.filter((id) => !nextIds.includes(id)); if (!added.length && !removed.length) return; setBusyId(u.id); try { for (const pid of added) await apiV26.projects.setPm(pid, u.id); for (const pid of removed) await apiV26.projects.setPm(pid, null); const names = nextIds .map((id) => data.lookup.projects.find((p) => p.id === id)?.name) .filter(Boolean) .join("، "); toast.success( nextIds.length ? `پروژه‌های «${u.first_name} ${u.last_name}» به‌روزرسانی شد: ${names} ✓` : `«${u.first_name} ${u.last_name}» از همه‌ی پروژه‌ها جدا شد`, ); await loadUsers(); await data.refresh(); refreshProcurementStore(); } catch (err) { toast.error(err instanceof Error ? err.message : "خطا در به‌روزرسانی پروژه‌ها"); await loadUsers(); // وضعیت واقعی سرور را برگردان } finally { setBusyId(null); } }; // ---------- actions: warehouses ---------- const addWarehouse = async (projectId: string) => { const name = (newWh[projectId] || "").trim(); if (!name) return toast.error("نام انبار الزامی است"); try { await apiV26.warehouses.create({ project_id: projectId, name, description: (newWhDesc[projectId] || "").trim() || null }); setNewWh((s) => ({ ...s, [projectId]: "" })); setNewWhDesc((s) => ({ ...s, [projectId]: "" })); toast.success(`انبار «${name}» اضافه شد ✓`); await data.refresh(); } catch (err) { toast.error(err instanceof Error ? err.message : "خطا در افزودن انبار"); } }; // انبار برای همیشه به پروژه‌ی والدش تعلق دارد — انتقال به پروژه‌ی دیگر وجود ندارد؛ // انبار فقط با حذف و ساخت دوباره زیر پروژه‌ی موردنظر جابه‌جا می‌شود. const removeWarehouse = async (id: string) => { if (!confirm("انبار حذف شود؟ (انبارهای دارای سابقه تراکنش قابل حذف نیستند)")) return; try { await apiV26.warehouses.delete(id); toast.success("انبار حذف شد"); await data.refresh(); } catch (err) { toast.error(err instanceof Error ? err.message : "خطا در حذف انبار"); } }; // مدیرِ مسئول یک پروژه (1:N — کاربر ممکن است مدیر چند پروژه باشد) const pmOf = (projectId: string) => users.find( (u) => u.role === "pm" && (u.projects?.some((p) => p.id === projectId) || u.project_id === projectId), ) || null; return (
ℹ️ کاربران از Casdoor همگام می‌شوند و نقش‌ها اینجا (فقط توسط ادمین) تعیین می‌شوند؛ مدیر تدارکات فقط پروژه‌ی مدیران پروژه را تعیین/جابه‌جا می‌کند. پروژه‌ها از سیستم تدارکات می‌آیند و هر مدیر پروژه به همه انبارهای پروژه خودش دسترسی دارد. همه تغییرات فوراً در داشبورد و فرم‌ها اعمال می‌شود.
{/* ---------- کاربران و مدیران پروژه ---------- */}

👥 کاربران و مدیران پروژه

{loadingUsers ? (
در حال بارگذاری کاربران…
) : !users.length ? (
کاربری ثبت نشده است — کاربران پس از اولین ورود (Casdoor) به‌صورت خودکار ساخته می‌شوند؛ یا دکمه‌ی «همگام‌سازی از Casdoor» را بزنید.
) : (
{users.map((u) => ( ))}
نام نقش پروژه (مدیر پروژه) عملیات
{u.first_name} {u.last_name} {u.casdoor_id} {canChangeRoles ? ( ) : ( {u.role ? ROLE_LABELS[u.role] : "بدون نقش"} )} {!canChangeRoles && u.role && ( (تغییر نقش فقط توسط ادمین) )} {u.role === "pm" ? ( ({ id: p.id, name: p.name }))} values={assignedProjectsOf(u).map((p) => p.id)} onChange={(ids) => void syncUserProjects(u, ids)} disabled={busyId === u.id} /> ) : ( — )} {u.role === "pm" && u.project_id && ( فعال )}
«بدون نقش» = حذف مدیر پروژه (از همه‌ی پروژه‌ها هم جدا می‌شود). هر مدیر پروژه می‌تواند به چند پروژه متصل باشد؛ حذف هر چیپ (×)، فقط همان پروژه را جدا می‌کند و اتصال به یک پروژه، مدیرِ قبلیِ آن پروژه را جابه‌جا می‌کند.
)}
{/* ---------- پروژه‌ها و انبارها ---------- */}
{data.lookup.projects.map((p) => { const whs = data.warehouses.filter((w) => w.project_id === p.id); const pm = pmOf(p.id); return (

🏢 {p.name}

{faNum(whs.length)} انبار

👤 مدیر پروژه

{pm ? (
{pm.first_name} {pm.last_name} (مدیر مسئول پروژه)
) : (
مدیری تعیین نشده است — از جدول «کاربران و مدیران پروژه» بالا، نقش مدیر پروژه را بدهید و به این پروژه متصل کنید.
)}

🏬 انبارهای پروژه

{!whs.length ? (
انباری ندارد — از فرم زیر اضافه کنید.
) : (
{whs.map((w) => (
{w.name} {w.description || ""}
))}
)}
setNewWh((s) => ({ ...s, [p.id]: e.target.value }))} /> setNewWhDesc((s) => ({ ...s, [p.id]: e.target.value }))} />
); })}
); }