From c7d860f431b99cfd40066a0ae4c4d0c6f04e4b22 Mon Sep 17 00:00:00 2001 From: Milad Date: Sun, 27 Sep 2026 18:42:05 +0330 Subject: [PATCH] feat(inventory): limit supervisor access to own project inventory and remove rectangular change inventory icon --- backend/app/routers/inventory.py | 84 ++++++++++++++----- backend/app/routers/pm.py | 57 +++++++++---- frontend/src/App.tsx | 5 +- .../src/components/layout/MobileTabBar.tsx | 7 +- frontend/src/pages/Inventory/ArrivalsPage.tsx | 16 ++-- .../src/pages/Inventory/DailyControlPage.tsx | 16 ++-- .../src/pages/Inventory/InventoryTopNav.tsx | 13 +-- .../src/pages/Inventory/WarehousesPage.tsx | 9 +- frontend/src/pages/Inventory/panelMenu.ts | 4 +- 9 files changed, 138 insertions(+), 73 deletions(-) diff --git a/backend/app/routers/inventory.py b/backend/app/routers/inventory.py index 416c4c3..fcc6a14 100644 --- a/backend/app/routers/inventory.py +++ b/backend/app/routers/inventory.py @@ -58,6 +58,7 @@ class Scope: role: str project_id: uuid.UUID | None actor: str | None + project_ids: list[uuid.UUID] | None = None async def inventory_scope( @@ -79,7 +80,7 @@ async def inventory_scope( raise ForbiddenError("نقش شما هنوز در سیستم تعیین نشده است") # مدیر تدارکات و مدیر پروژه: دسترسی کامل به پنل انبار، اما فقط‌خواندنی در تغییر موجودی if user.role in ("procurement_manager", "pm"): - return Scope(role="manager", project_id=project_id, actor=user.display_name) + return Scope(role="manager", project_id=project_id, actor=user.display_name, project_ids=list(user.project_ids or [])) if user.role == "supervisor": allowed = list(user.project_ids or []) if not allowed: @@ -87,8 +88,8 @@ async def inventory_scope( if project_id is not None: if project_id not in allowed: raise ForbiddenError("این پروژه به شما تخصیص داده نشده است") - return Scope(role="supervisor", project_id=project_id, actor=user.display_name) - return Scope(role="supervisor", project_id=allowed[0], actor=user.display_name) + return Scope(role="supervisor", project_id=project_id, actor=user.display_name, project_ids=allowed) + return Scope(role="supervisor", project_id=allowed[0], actor=user.display_name, project_ids=allowed) if user.role == "admin": return Scope(role="admin", project_id=project_id, actor=user.display_name) raise ValidationError("نقش نامعتبر است", field="role") @@ -214,7 +215,15 @@ async def list_warehouses( ): stmt = select(Warehouse) if scope.role in (ROLE_PM, "supervisor"): - stmt = stmt.where(Warehouse.project_id == scope.project_id) # force PM / supervisor scope + allowed = scope.project_ids or ([scope.project_id] if scope.project_id else []) + if project_id is not None: + if project_id not in allowed: + raise ForbiddenError("این پروژه به شما تخصیص داده نشده است") + stmt = stmt.where(Warehouse.project_id == project_id) + elif allowed: + stmt = stmt.where(Warehouse.project_id.in_(allowed)) + else: + stmt = stmt.where(Warehouse.project_id == scope.project_id) elif project_id is not None: stmt = stmt.where(Warehouse.project_id == project_id) stmt = stmt.order_by(Warehouse.name) @@ -246,9 +255,11 @@ async def create_warehouse( scope: Scope = Depends(inventory_scope), db: AsyncSession = Depends(get_db), ): - # PM: فقط پروژه خودش — Admin: ساختار را مدیریت می‌کند (هر پروژه) - if scope.role == ROLE_PM and payload.project_id != scope.project_id: - raise ValidationError("PM فقط می‌تواند برای پروژه خودش انبار بسازد", field="project_id") + # PM / سرپرست: فقط پروژه خودش — Admin: ساختار را مدیریت می‌کند (هر پروژه) + if scope.role in (ROLE_PM, "supervisor"): + allowed = scope.project_ids or ([scope.project_id] if scope.project_id else []) + if payload.project_id not in allowed: + raise ValidationError("شما فقط می‌توانید برای پروژه تخصیص‌یافته به خود انبار بسازید", field="project_id") if await db.get(Project, payload.project_id) is None: raise NotFoundError("پروژه یافت نشد", field="project_id") wh = Warehouse(project_id=payload.project_id, name=payload.name, description=payload.description) @@ -265,8 +276,9 @@ async def update_warehouse( db: AsyncSession = Depends(get_db), ): wh = await get_warehouse_or_404(db, warehouse_id) - if scope.role == ROLE_PM: - if scope.project_id != wh.project_id: + if scope.role in (ROLE_PM, "supervisor"): + allowed = scope.project_ids or ([scope.project_id] if scope.project_id else []) + if wh.project_id not in allowed: raise ForbiddenError("این انبار متعلق به پروژه شما نیست") if payload.name is not None: wh.name = payload.name @@ -290,8 +302,10 @@ async def delete_warehouse( db: AsyncSession = Depends(get_db), ): wh = await get_warehouse_or_404(db, warehouse_id) - if scope.role == ROLE_PM and scope.project_id != wh.project_id: - raise ForbiddenError("این انبار متعلق به پروژه شما نیست") + if scope.role in (ROLE_PM, "supervisor"): + allowed = scope.project_ids or ([scope.project_id] if scope.project_id else []) + if wh.project_id not in allowed: + raise ForbiddenError("این انبار متعلق به پروژه شما نیست") # inventory rows cascade via DDL; transactions are RESTRICTed through # inventory cascade — block deletion if any stock history exists. linked = await db.scalar( @@ -322,9 +336,21 @@ async def list_inventory( ): stmt = select(InventoryItem) if scope.role in (ROLE_PM, "supervisor"): - stmt = stmt.join(Warehouse, Warehouse.id == InventoryItem.warehouse_id).where( - Warehouse.project_id == scope.project_id - ) + allowed = scope.project_ids or ([scope.project_id] if scope.project_id else []) + if project_id is not None: + if project_id not in allowed: + raise ForbiddenError("این پروژه به شما تخصیص داده نشده است") + stmt = stmt.join(Warehouse, Warehouse.id == InventoryItem.warehouse_id).where( + Warehouse.project_id == project_id + ) + elif allowed: + stmt = stmt.join(Warehouse, Warehouse.id == InventoryItem.warehouse_id).where( + Warehouse.project_id.in_(allowed) + ) + else: + stmt = stmt.join(Warehouse, Warehouse.id == InventoryItem.warehouse_id).where( + Warehouse.project_id == scope.project_id + ) elif project_id is not None: stmt = stmt.join(Warehouse, Warehouse.id == InventoryItem.warehouse_id).where( Warehouse.project_id == project_id @@ -387,7 +413,15 @@ async def list_transactions( InventoryItem, InventoryItem.id == InventoryTransaction.inventory_id ).join(Warehouse, Warehouse.id == InventoryItem.warehouse_id) if scope.role in (ROLE_PM, "supervisor"): - stmt = stmt.where(Warehouse.project_id == scope.project_id) + allowed = scope.project_ids or ([scope.project_id] if scope.project_id else []) + if project_id is not None: + if project_id not in allowed: + raise ForbiddenError("این پروژه به شما تخصیص داده نشده است") + stmt = stmt.where(Warehouse.project_id == project_id) + elif allowed: + stmt = stmt.where(Warehouse.project_id.in_(allowed)) + else: + stmt = stmt.where(Warehouse.project_id == scope.project_id) elif project_id is not None: stmt = stmt.where(Warehouse.project_id == project_id) if warehouse_id is not None: @@ -452,8 +486,10 @@ async def inventory_history( if item is None: raise NotFoundError("موجودی یافت نشد") wh = await get_warehouse_or_404(db, item.warehouse_id) - if scope.role in (ROLE_PM, "supervisor") and wh.project_id != scope.project_id: - raise ForbiddenError("این موجودی متعلق به پروژه شما نیست") + if scope.role in (ROLE_PM, "supervisor"): + allowed = scope.project_ids or ([scope.project_id] if scope.project_id else []) + if wh.project_id not in allowed: + raise ForbiddenError("این موجودی متعلق به پروژه شما نیست") txs = (await db.execute( select(InventoryTransaction) .where(InventoryTransaction.inventory_id == inventory_id) @@ -478,12 +514,14 @@ async def create_transaction( # commits on success / rolls back on failure. with nullcontext(): wh = await get_warehouse_or_404(db, payload.warehouse_id) - if scope.project_id is not None and wh.project_id != scope.project_id: - raise ForbiddenError("این انبار متعلق به پروژه شما نیست") - if payload.dest_warehouse_id is not None: - dest = await get_warehouse_or_404(db, payload.dest_warehouse_id) - if scope.project_id is not None and dest.project_id != scope.project_id: - raise ForbiddenError("انبار مقصد متعلق به پروژه شما نیست") + allowed = scope.project_ids or ([scope.project_id] if scope.project_id else []) + if scope.role in (ROLE_PM, "supervisor"): + if wh.project_id not in allowed: + raise ForbiddenError("این انبار متعلق به پروژه شما نیست") + if payload.dest_warehouse_id is not None: + dest = await get_warehouse_or_404(db, payload.dest_warehouse_id) + if dest.project_id not in allowed: + raise ForbiddenError("انبار مقصد متعلق به پروژه شما نیست") txs = await apply_transaction(db, payload, performed_by=scope.actor) await audit_record( db, diff --git a/backend/app/routers/pm.py b/backend/app/routers/pm.py index f8c1c3f..b075ffb 100644 --- a/backend/app/routers/pm.py +++ b/backend/app/routers/pm.py @@ -71,16 +71,17 @@ class ArrivalScope: user_id: uuid.UUID | None project_id: uuid.UUID | None # PM's active project; admin: optional filter display_name: str + project_ids: list[uuid.UUID] | None = None async def arrivals_scope( request: Request, - project_id: uuid.UUID | None = Query(default=None, description="پروژه فعال (PM: الزامی برای داده‌های پروژه)"), + project_id: uuid.UUID | None = Query(default=None, description="پروژه فعال (PM / سرپرست: داده‌های پروژه)"), db: AsyncSession = Depends(get_db), ) -> ArrivalScope: """Resolve the arrivals caller — Casdoor JWT first, dev headers as fallback. - PM → scoped to the requested project, which must be one they manage + PM / supervisor → scoped to the requested project, which must be one they manage (projects.pm_user_id, 1:N); without the param, their first project. Admin / procurement manager → all projects (param = optional filter). """ @@ -89,19 +90,19 @@ async def arrivals_scope( if ctx is not None: if ctx.role is None: raise ForbiddenError("نقش شما هنوز در سیستم تعیین نشده است") - if ctx.role == "pm": + if ctx.role in ("pm", "supervisor"): allowed = list(ctx.project_ids or []) if project_id is not None: if project_id not in allowed: raise ForbiddenError("این پروژه به شما تخصیص داده نشده است") - return ArrivalScope(role="pm", user_id=ctx.user.id, project_id=project_id, display_name=ctx.display_name) + return ArrivalScope(role="pm", user_id=ctx.user.id, project_id=project_id, display_name=ctx.display_name, project_ids=allowed) if not allowed: raise ForbiddenError("شما به هیچ پروژه‌ای متصل نیستید (projects.pm_user_id)") - return ArrivalScope(role="pm", user_id=ctx.user.id, project_id=allowed[0], display_name=ctx.display_name) + return ArrivalScope(role="pm", user_id=ctx.user.id, project_id=allowed[0], display_name=ctx.display_name, project_ids=allowed) if ctx.role in ("admin", "procurement_manager"): return ArrivalScope(role="admin", user_id=ctx.user.id, project_id=project_id, display_name=ctx.display_name) - raise ForbiddenError("این بخش فقط برای مدیر پروژه یا مدیر تدارکات است") - # Dev fallback: X-User-Role: pm | admin (procurement_manager maps to + raise ForbiddenError("این بخش فقط برای مدیر یا سرپرست پروژه و مدیر تدارکات است") + # Dev fallback: X-User-Role: pm | supervisor | admin (procurement_manager maps to # admin) — ONLY when DEV_LOGIN_ENABLED (local testing without Casdoor). # Disabled by default: an ungated header identity in production would # let anyone confirm receipts as PM. @@ -110,8 +111,10 @@ async def arrivals_scope( role = (request.headers.get("x-user-role") or "").strip().lower() if role == "procurement_manager": role = "admin" + elif role == "supervisor": + role = "pm" if role not in ("pm", "admin"): - raise ForbiddenError("این بخش فقط برای مدیر پروژه یا مدیر تدارکات است — هدر X-User-Role را بفرستید") + raise ForbiddenError("این بخش فقط برای مدیر یا سرپرست پروژه و مدیر تدارکات است — هدر X-User-Role را بفرستید") # The explicit query param wins; otherwise the dev header carries it. if project_id is None and role == "pm": raw_project = request.headers.get("x-project-id") @@ -189,10 +192,17 @@ async def pm_on_the_way( .outerjoin(Project, Project.id == ItemDecision.project_id) .where(ItemDecision.status == "chosen") ) - if ctx.role == "pm" and ctx.project_id is None: - raise ForbiddenError("شما به هیچ پروژه‌ای متصل نیستید (projects.pm_user_id)") - if ctx.project_id is not None: - # PM: active project; admin/manager: optional project filter + if ctx.role == "pm": + allowed = ctx.project_ids or ([ctx.project_id] if ctx.project_id else []) + if not allowed: + raise ForbiddenError("شما به هیچ پروژه‌ای متصل نیستید (projects.pm_user_id)") + if ctx.project_id is not None: + if ctx.project_id not in allowed: + raise ForbiddenError("این پروژه به شما تخصیص داده نشده است") + stmt = stmt.where(ItemDecision.project_id == ctx.project_id) + else: + stmt = stmt.where(ItemDecision.project_id.in_(allowed)) + elif ctx.project_id is not None: stmt = stmt.where(ItemDecision.project_id == ctx.project_id) decisions = (await db.execute(stmt.order_by(ItemDecision.decided_at.desc().nulls_last()))).all() @@ -243,8 +253,12 @@ async def pm_confirm_receipt( decision = await db.get(ItemDecision, payload.item_decision_id) if decision is None or decision.status != "chosen": raise NotFoundError("تصمیم خرید یافت نشد", field="item_decision_id") - if ctx.project_id is not None and decision.project_id != ctx.project_id: - raise ForbiddenError("این قلم متعلق به پروژه فعال نیست") + if ctx.role == "pm": + allowed = ctx.project_ids or ([ctx.project_id] if ctx.project_id else []) + if decision.project_id not in allowed: + raise ForbiddenError("این قلم متعلق به پروژه شما نیست") + if ctx.project_id is not None and decision.project_id != ctx.project_id: + raise ForbiddenError("این قلم متعلق به پروژه فعال نیست") li = await db.get(LineItem, decision.line_item_id) quote = await db.get(Quote, li.quote_id) @@ -451,10 +465,17 @@ async def pm_receipts( .join(Quote, Quote.id == LineItem.quote_id) .join(Project, Project.id == ItemDecision.project_id) ) - if ctx.role == "pm" and ctx.project_id is None: - raise ForbiddenError("شما به هیچ پروژه‌ای متصل نیستید (projects.pm_user_id)") - if ctx.project_id is not None: - # PM: active project; admin/manager: optional project filter + if ctx.role == "pm": + allowed = ctx.project_ids or ([ctx.project_id] if ctx.project_id else []) + if not allowed: + raise ForbiddenError("شما به هیچ پروژه‌ای متصل نیستید (projects.pm_user_id)") + if ctx.project_id is not None: + if ctx.project_id not in allowed: + raise ForbiddenError("این پروژه به شما تخصیص داده نشده است") + stmt = stmt.where(ItemDecision.project_id == ctx.project_id) + else: + stmt = stmt.where(ItemDecision.project_id.in_(allowed)) + elif ctx.project_id is not None: stmt = stmt.where(ItemDecision.project_id == ctx.project_id) rows = (await db.execute(stmt.order_by(ItemReceipt.received_at.desc()))).all() receipts = [r.ItemReceipt for r in rows] diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 384f91d..885828d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -142,10 +142,11 @@ function RequireInventoryChange({ children }: { children: React.ReactNode }) { return <>{children}; } -// دسترسی مدیریت انبارها: فقط مدیران و ادمین (سرپرست پروژه فقط می‌تواند موجودی تغییر دهد) +// دسترسی مدیریت انبارها: مدیران، ادمین و سرپرست پروژه (سرپرست پروژه منحصراً انبارهای پروژه خودش را می‌بیند) function RequireWarehouseManage({ children }: { children: React.ReactNode }) { const user = useAuthStore((s) => s.user); - if (user?.role === "supervisor") { + const allowed = user?.role === "admin" || user?.role === "procurement_manager" || user?.role === "pm" || user?.role === "supervisor"; + if (!allowed) { return ; } return <>{children}; diff --git a/frontend/src/components/layout/MobileTabBar.tsx b/frontend/src/components/layout/MobileTabBar.tsx index 1055cdb..8e36d46 100644 --- a/frontend/src/components/layout/MobileTabBar.tsx +++ b/frontend/src/components/layout/MobileTabBar.tsx @@ -82,16 +82,17 @@ export function MobileTabBar() { 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 isSupervisor = role === "supervisor"; const isOffice = role === "admin" || role === "procurement_manager"; if (pathname.startsWith("/inventory")) { if (isPM) return ; - if (isOffice) return ; + if (isOffice || isSupervisor) return ; return null; } + // سرپرست پروژه به بخش‌های تدارکات/دفتر دسترسی ندارد + if (isSupervisor) return null; if (pathname === "/wizard") return null; const onOfficePage = pathname === "/" || diff --git a/frontend/src/pages/Inventory/ArrivalsPage.tsx b/frontend/src/pages/Inventory/ArrivalsPage.tsx index bddd4bb..38890ae 100644 --- a/frontend/src/pages/Inventory/ArrivalsPage.tsx +++ b/frontend/src/pages/Inventory/ArrivalsPage.tsx @@ -23,6 +23,8 @@ export function ArrivalsPage() { const user = useAuthStore((s) => s.user); const isAdmin = user?.role === "admin" || user?.role === "procurement_manager"; const isPM = user?.role === "pm"; + const isSupervisor = user?.role === "supervisor"; + const canAccess = isAdmin || isPM || isSupervisor; const [items, setItems] = useState([]); const [warehouses, setWarehouses] = useState([]); @@ -33,9 +35,9 @@ export function ArrivalsPage() { const load = useCallback(async () => { try { - const activeProject = isPM ? user?.project_id || undefined : undefined; + const activeProject = (isPM || isSupervisor) ? user?.project_id || undefined : undefined; const [arr, whs] = await Promise.all([ - // پروژه‌ی فعال صریحاً ارسال می‌شود؛ بک‌اند تخصیص PM را بررسی می‌کند + // پروژه‌ی فعال صریحاً ارسال می‌شود؛ بک‌اند تخصیص PM / سرپرست را بررسی می‌کند apiV26.pm.onTheWay(activeProject), apiV26.warehouses.list(activeProject), ]); @@ -46,13 +48,13 @@ export function ArrivalsPage() { } finally { setLoading(false); } - }, [isPM, user?.project_id]); + }, [isPM, isSupervisor, user?.project_id]); useEffect(() => { - if (!isAdmin && !isPM) return; + if (!canAccess) return; setLoading(true); void load(); - }, [isAdmin, isPM, load]); + }, [canAccess, load]); // سفارش‌ها: گروه‌بندی اقلام به‌ازای هر پیشنهاد (فقط سفارش‌های دارای قلمِ در راه/ناقص) const orders = useMemo(() => { @@ -212,12 +214,12 @@ export function ArrivalsPage() { } }; - if (!isAdmin && !isPM) { + if (!canAccess) { return (
- +
); diff --git a/frontend/src/pages/Inventory/DailyControlPage.tsx b/frontend/src/pages/Inventory/DailyControlPage.tsx index 560dad9..5868a69 100644 --- a/frontend/src/pages/Inventory/DailyControlPage.tsx +++ b/frontend/src/pages/Inventory/DailyControlPage.tsx @@ -41,10 +41,12 @@ export function DailyControlPage() { const txByDay = useMemo(() => { const map = new Map(); + const allowedWhIds = new Set(data.warehouses.map((w) => w.id)); const projWhIds = projFilter ? new Set(data.warehouses.filter((w) => String(w.project_id) === String(projFilter)).map((w) => w.id)) : null; data.transactions.forEach((t) => { + if (!isAdmin && !allowedWhIds.has(t.warehouse_id)) return; if (projWhIds && !projWhIds.has(t.warehouse_id)) return; const k = gregKey(new Date(t.transaction_date)); const arr = map.get(k) || []; @@ -53,7 +55,7 @@ export function DailyControlPage() { }); map.forEach((arr) => arr.sort((a, b) => new Date(a.transaction_date).getTime() - new Date(b.transaction_date).getTime())); return map; - }, [data.transactions, data.warehouses, projFilter]); + }, [data.transactions, data.warehouses, projFilter, isAdmin]); const cells = useMemo(() => { const [ty, tm] = gregorianToJalali(anchor.getFullYear(), anchor.getMonth() + 1, anchor.getDate()); @@ -172,11 +174,15 @@ export function DailyControlPage() { ))} ) : ( - پروژه فعال + + {user?.project_name ? `پروژه: ${user.project_name}` : "پروژه فعال"} + + )} + {isAdmin && ( + + {projFilter ? "نمایش برای پروژه انتخابی" : "نمایش تمام پروژه‌ها"} + )} - - {projFilter ? "نمایش برای پروژه انتخابی" : "نمایش تمام پروژه‌ها"} - s.user); const isAdmin = user?.role === "admin"; const isManager = user?.role === "procurement_manager" || user?.role === "pm" || isAdmin; + const isSupervisor = user?.role === "supervisor"; + const canViewTabs = isManager || isSupervisor; const items: { label: string; to: string; active: boolean }[] = []; - // سرپرست پروژه از دکمه شناور گرد (+) در پایین صفحه استفاده می‌کند و دکمه مستطیلی بالای صفحه برای او نمایش داده نمی‌شود - if (isAdmin) { - items.push({ - label: "تغییر موجودی", - to: "/inventory/change", - active: pathname.startsWith("/inventory/change"), - }); - } - if (isManager) { + // برای تغییر موجودی از همان دکمه شناور دایره‌ای آبی (+) استفاده می‌شود و دکمه مستطیلی در بالای صفحه حذف شد + if (canViewTabs) { items.push({ label: "کنترل روزانه", to: "/inventory/daily", active: pathname.startsWith("/inventory/daily") }); items.push({ label: "انبارها", to: "/inventory/warehouses", active: pathname.startsWith("/inventory/warehouses") }); items.push({ label: "کالاهای در راه", to: "/inventory/arrivals", active: pathname.startsWith("/inventory/arrivals") }); diff --git a/frontend/src/pages/Inventory/WarehousesPage.tsx b/frontend/src/pages/Inventory/WarehousesPage.tsx index bbebe8e..4e4e0c2 100644 --- a/frontend/src/pages/Inventory/WarehousesPage.tsx +++ b/frontend/src/pages/Inventory/WarehousesPage.tsx @@ -14,7 +14,8 @@ export function WarehousesPage() { const navigate = useNavigate(); const user = useAuthStore((s) => s.user); const data = useInventoryData(); - const canManage = user?.role === "admin" || user?.role === "procurement_manager" || user?.role === "pm"; + const isSupervisor = user?.role === "supervisor"; + const canManage = user?.role === "admin" || user?.role === "procurement_manager" || user?.role === "pm" || isSupervisor; const [newName, setNewName] = useState(""); const [newDesc, setNewDesc] = useState(""); @@ -32,7 +33,7 @@ export function WarehousesPage() {
🔒

دسترسی محدود

- مدیریت و تعریف انبارها مخصوص مدیران سیستم است. سرپرست پروژه فقط به ثبت تغییرات موجودی دسترسی دارد. + دسترسی به این بخش برای حساب کاربری شما فعال نیست.

@@ -43,7 +44,7 @@ export function WarehousesPage() { const addWarehouse = async () => { const name = newName.trim(); if (!name) return toast.error("نام انبار الزامی است"); - const projectId = selectedProjectId || user?.project_id || data.lookup.projects[0]?.id; + const projectId = isSupervisor ? user?.project_id : (selectedProjectId || user?.project_id || data.lookup.projects[0]?.id); if (!projectId) return toast.error("پروژه انبار را انتخاب کنید"); try { await apiV26.warehouses.create({ project_id: projectId, name, description: newDesc.trim() || null }); @@ -92,7 +93,7 @@ export function WarehousesPage() {

+ افزودن انبار جدید

- {data.lookup.projects.length > 1 && ( + {!isSupervisor && data.lookup.projects.length > 1 && (