feat(inventory): role-aware header navigation - PM top nav without hamburger, admin top nav plus compact account menu
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation, useSearchParams } from "react-router-dom";
|
||||||
import { Header, PageLayout } from "../../components/layout/Header";
|
import { Header, PageLayout } from "../../components/layout/Header";
|
||||||
import { EmptyState } from "../../components/EmptyState";
|
import { EmptyState } from "../../components/EmptyState";
|
||||||
import { Badge } from "../../components/ui/badge";
|
import { Badge } from "../../components/ui/badge";
|
||||||
import { useAuthStore } from "../../store/auth";
|
import { useAuthStore } from "../../store/auth";
|
||||||
import { PanelMenu } from "../../components/PanelMenu";
|
import { PanelMenu } from "../../components/PanelMenu";
|
||||||
import { panelMenuItems, useLogout } from "./panelMenu";
|
import { panelMenuItems, accountMenuItems, useLogout } from "./panelMenu";
|
||||||
|
import { InventoryTopNav } from "./InventoryTopNav";
|
||||||
import { useInventoryData, brandName, catName, materialName, unitName, whName } from "./useInventoryData";
|
import { useInventoryData, brandName, catName, materialName, unitName, whName } from "./useInventoryData";
|
||||||
import type { InventoryRow, InventoryTransaction, TransactionType } from "../../lib/types";
|
import type { InventoryRow, InventoryTransaction, TransactionType } from "../../lib/types";
|
||||||
import { faNum } from "../../lib/formatters";
|
import { faNum } from "../../lib/formatters";
|
||||||
@@ -52,9 +53,11 @@ export function InventoryPage() {
|
|||||||
const { warehouses, inventory, transactions, lookup, loading } = useInventoryData();
|
const { warehouses, inventory, transactions, lookup, loading } = useInventoryData();
|
||||||
const isAdmin = user?.role === "admin" || user?.role === "procurement_manager"; // مدیر تدارکات = ادمین دفتر
|
const isAdmin = user?.role === "admin" || user?.role === "procurement_manager"; // مدیر تدارکات = ادمین دفتر
|
||||||
const isPM = user?.role === "pm";
|
const isPM = user?.role === "pm";
|
||||||
|
// پرش عمیق از «انبارها»: /inventory?warehouse=<id> → داشبورد همان انبار از پیش فیلتر شده
|
||||||
|
const [params] = useSearchParams();
|
||||||
|
|
||||||
const [filters, setFilters] = useState<{ category: string; warehouse: string; material: string; project: string }>({
|
const [filters, setFilters] = useState<{ category: string; warehouse: string; material: string; project: string }>({
|
||||||
category: "", warehouse: "", material: "", project: "",
|
category: "", warehouse: params.get("warehouse") || "", material: "", project: "",
|
||||||
});
|
});
|
||||||
const [sort, setSort] = useState<"asc" | "desc" | "">("");
|
const [sort, setSort] = useState<"asc" | "desc" | "">("");
|
||||||
const [expandedKey, setExpandedKey] = useState<string | null>(null);
|
const [expandedKey, setExpandedKey] = useState<string | null>(null);
|
||||||
@@ -134,8 +137,9 @@ export function InventoryPage() {
|
|||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Header
|
<Header
|
||||||
title="سامانه مدیریت انبار"
|
title="سامانه مدیریت انبار"
|
||||||
sub={isAdmin ? "فقطخواندنی" : user?.project_name ? `مدیر پروژه — ${user.project_name}` : "مدیر پروژه"}
|
sub={isAdmin ? "مدیر تدارکات — همه پروژهها" : user?.project_name ? `مدیر پروژه — ${user.project_name}` : "مدیر پروژه"}
|
||||||
menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />}
|
menu={isAdmin ? <PanelMenu title="پنل مدیریت انبار" items={accountMenuItems(logout)} /> : undefined}
|
||||||
|
actions={isAdmin || isPM ? <InventoryTopNav pathname={location.pathname} onLogout={logout} variant={isAdmin ? "admin" : "pm"} /> : undefined}
|
||||||
/>
|
/>
|
||||||
<PageLayout className="flex flex-col gap-3">
|
<PageLayout className="flex flex-col gap-3">
|
||||||
{/* فیلترها */}
|
{/* فیلترها */}
|
||||||
@@ -206,6 +210,9 @@ export function InventoryPage() {
|
|||||||
✕ پاک کردن فیلترها
|
✕ پاک کردن فیلترها
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{filters.warehouse && (
|
||||||
|
<Badge variant="offered">🏬 {whName(warehouses, filters.warehouse)}</Badge>
|
||||||
|
)}
|
||||||
<Badge variant="outline">{faNum(groups.length)} قلم کالا</Badge>
|
<Badge variant="outline">{faNum(groups.length)} قلم کالا</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
// ناوبری بالای صفحه در پنل انبار — جایگزین لینکهای منوی همبرگری
|
||||||
|
// (مثل سیستم تدارکات: لینکها در بالای صفحه)
|
||||||
|
//
|
||||||
|
// - variant="pm": همه بخشهای مدیر پروژه + «دفتر تدارکات» و «خروج از حساب»
|
||||||
|
// (منوی همبرگری حذف میشود).
|
||||||
|
// - variant="admin": فقط بخشهای مدیر تدارکات/ادمین — «بازگشت به دفتر» و
|
||||||
|
// «خروج از حساب» در منوی همبرگری میمانند.
|
||||||
|
export function InventoryTopNav({
|
||||||
|
pathname,
|
||||||
|
onLogout,
|
||||||
|
variant = "pm",
|
||||||
|
}: {
|
||||||
|
pathname: string;
|
||||||
|
onLogout: () => void;
|
||||||
|
variant?: "pm" | "admin";
|
||||||
|
}) {
|
||||||
|
const items =
|
||||||
|
variant === "admin"
|
||||||
|
? [
|
||||||
|
{ label: "داشبورد انبار", to: "/inventory", active: pathname === "/inventory" },
|
||||||
|
{ label: "کنترل روزانه", to: "/inventory/daily", active: pathname.startsWith("/inventory/daily") },
|
||||||
|
{ label: "کالاهای در راه", to: "/inventory/arrivals", active: pathname.startsWith("/inventory/arrivals") },
|
||||||
|
{ label: "سازماندهی", to: "/inventory/org", active: pathname.startsWith("/inventory/org") },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ label: "داشبورد انبار", to: "/inventory", active: pathname === "/inventory" },
|
||||||
|
{ label: "تغییر موجودی", to: "/inventory/change", active: pathname.startsWith("/inventory/change") },
|
||||||
|
{ label: "کنترل روزانه", to: "/inventory/daily", active: pathname.startsWith("/inventory/daily") },
|
||||||
|
{ label: "انبارها", to: "/inventory/warehouses", active: pathname.startsWith("/inventory/warehouses") },
|
||||||
|
{ label: "کالاهای در راه", to: "/inventory/arrivals", active: pathname.startsWith("/inventory/arrivals") },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.map((it) => (
|
||||||
|
<Link
|
||||||
|
key={it.to}
|
||||||
|
to={it.to}
|
||||||
|
className={cn(
|
||||||
|
"btn-line-file no-underline",
|
||||||
|
it.active && "border-accent bg-accent text-white hover:bg-accent-dark",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{it.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
{variant === "pm" && (
|
||||||
|
<>
|
||||||
|
<Link to="/" className="btn-line-file no-underline">
|
||||||
|
دفتر تدارکات
|
||||||
|
</Link>
|
||||||
|
<button type="button" onClick={onLogout} className="btn-line-file cursor-pointer border-red/30 text-red hover:bg-red/10">
|
||||||
|
خروج از حساب
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,22 +1,24 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Header, PageLayout } from "../../components/layout/Header";
|
import { Header, PageLayout } from "../../components/layout/Header";
|
||||||
import { useAuthStore } from "../../store/auth";
|
import { useAuthStore } from "../../store/auth";
|
||||||
import { PanelMenu } from "../../components/PanelMenu";
|
import { PanelMenu } from "../../components/PanelMenu";
|
||||||
import { panelMenuItems, useLogout } from "./panelMenu";
|
import { panelMenuItems, useLogout } from "./panelMenu";
|
||||||
|
import { InventoryTopNav } from "./InventoryTopNav";
|
||||||
import { apiV26 } from "../../lib/api";
|
import { apiV26 } from "../../lib/api";
|
||||||
import { useInventoryData, projName } from "./useInventoryData";
|
import { useInventoryData, projName } from "./useInventoryData";
|
||||||
import { faNum } from "../../lib/formatters";
|
import { faNum } from "../../lib/formatters";
|
||||||
import { isoToJalaliStr } from "../../lib/jalali";
|
import { isoToJalaliStr } from "../../lib/jalali";
|
||||||
|
|
||||||
// انبارها — پورت کامل نسخهی ارسالی (مدیر پروژه: افزودن/ویرایش/حذف)
|
// انبارها — مدیر پروژه: مشاهده انبارهای پروژه خودش + افزودن/ویرایش/حذف (ادمین دسترسی ندارد)
|
||||||
export function WarehousesPage() {
|
export function WarehousesPage() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const logout = useLogout();
|
const logout = useLogout();
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
const data = useInventoryData();
|
const data = useInventoryData();
|
||||||
const isAdmin = user?.role === "admin";
|
const isAdmin = user?.role === "admin" || user?.role === "procurement_manager";
|
||||||
const isPM = user?.role === "pm";
|
const isPM = user?.role === "pm";
|
||||||
|
|
||||||
const [newName, setNewName] = useState("");
|
const [newName, setNewName] = useState("");
|
||||||
@@ -25,6 +27,34 @@ export function WarehousesPage() {
|
|||||||
const [editName, setEditName] = useState("");
|
const [editName, setEditName] = useState("");
|
||||||
const [editDesc, setEditDesc] = useState("");
|
const [editDesc, setEditDesc] = useState("");
|
||||||
|
|
||||||
|
// ادمین / مدیر تدارکات به بخش «انبارها» دسترسی ندارد — مدیریت انبارها از «سازماندهی»
|
||||||
|
if (isAdmin) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<Header title="انبارها" menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />} />
|
||||||
|
<PageLayout>
|
||||||
|
<div className="rounded-xl border border-line bg-panel p-8 text-center">
|
||||||
|
<div className="text-3xl">🔒</div>
|
||||||
|
<h3 className="mt-2 text-[15px] font-bold">دسترسی ندارد</h3>
|
||||||
|
<p className="mt-1 text-[12.5px] text-muted">
|
||||||
|
بخش «انبارها» مخصوص مدیران پروژه است. مدیریت انبارها از بخش «سازماندهی» انجام میشود.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!isPM) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<Header title="انبارها" menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />} />
|
||||||
|
<PageLayout>
|
||||||
|
<div className="text-[13px] text-muted">این بخش فقط برای مدیر پروژه است.</div>
|
||||||
|
</PageLayout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const addWarehouse = async () => {
|
const addWarehouse = async () => {
|
||||||
const name = newName.trim();
|
const name = newName.trim();
|
||||||
if (!name) return toast.error("نام انبار الزامی است");
|
if (!name) return toast.error("نام انبار الزامی است");
|
||||||
@@ -69,15 +99,9 @@ export function WarehousesPage() {
|
|||||||
<Header
|
<Header
|
||||||
title="انبارها"
|
title="انبارها"
|
||||||
sub={user?.project_name || ""}
|
sub={user?.project_name || ""}
|
||||||
menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />}
|
actions={<InventoryTopNav pathname={location.pathname} onLogout={logout} />}
|
||||||
/>
|
/>
|
||||||
<PageLayout className="flex flex-col gap-3">
|
<PageLayout className="flex flex-col gap-3">
|
||||||
{isAdmin && (
|
|
||||||
<div className="rounded-lg border border-amber/30 bg-amber/5 px-3 py-2 text-[12px] text-amber">
|
|
||||||
🔒 نقش ادمین فقطخواندنی است — مدیریت انبارها با مدیران پروژه است.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isPM && (
|
{isPM && (
|
||||||
<div className="rounded-xl border border-line bg-panel p-3">
|
<div className="rounded-xl border border-line bg-panel p-3">
|
||||||
<h3 className="mb-2 text-[13px] font-bold">+ افزودن انبار جدید</h3>
|
<h3 className="mb-2 text-[13px] font-bold">+ افزودن انبار جدید</h3>
|
||||||
@@ -106,7 +130,12 @@ export function WarehousesPage() {
|
|||||||
const cnt = data.inventory.filter((r) => r.warehouse?.id === w.id).length;
|
const cnt = data.inventory.filter((r) => r.warehouse?.id === w.id).length;
|
||||||
const txc = data.transactions.filter((t) => t.warehouse_id === w.id).length;
|
const txc = data.transactions.filter((t) => t.warehouse_id === w.id).length;
|
||||||
return (
|
return (
|
||||||
<div key={w.id} className="rounded-xl border border-line bg-panel p-4">
|
<div
|
||||||
|
key={w.id}
|
||||||
|
className="cursor-pointer rounded-xl border border-line bg-panel p-4 transition-colors hover:border-accent/50 hover:bg-accent/5"
|
||||||
|
onClick={() => navigate(`/inventory?warehouse=${w.id}`)}
|
||||||
|
title="ورود به داشبورد این انبار (فیلترشده)"
|
||||||
|
>
|
||||||
<h3 className="text-[14px] font-bold">🏬 {w.name}</h3>
|
<h3 className="text-[14px] font-bold">🏬 {w.name}</h3>
|
||||||
<div className="mt-0.5 text-[11.5px] text-muted">{w.description || "—"}</div>
|
<div className="mt-0.5 text-[11.5px] text-muted">{w.description || "—"}</div>
|
||||||
<div className="mt-2 flex flex-col gap-1 text-[12px] text-muted">
|
<div className="mt-2 flex flex-col gap-1 text-[12px] text-muted">
|
||||||
@@ -115,21 +144,29 @@ export function WarehousesPage() {
|
|||||||
<span>تراکنشها: <b className="text-foreground">{faNum(txc)}</b></span>
|
<span>تراکنشها: <b className="text-foreground">{faNum(txc)}</b></span>
|
||||||
<span>تاریخ ایجاد: <b className="text-foreground">{isoToJalaliStr(w.created_at)}</b></span>
|
<span>تاریخ ایجاد: <b className="text-foreground">{isoToJalaliStr(w.created_at)}</b></span>
|
||||||
</div>
|
</div>
|
||||||
{isPM && (
|
<div className="mt-3 flex items-center gap-1.5 border-t border-line pt-2">
|
||||||
<div className="mt-3 flex gap-1.5 border-t border-line pt-2">
|
<button
|
||||||
<button
|
className="rounded-lg border border-accent/40 bg-accent/5 px-3 py-1 text-[11.5px] font-bold text-accent"
|
||||||
className="rounded-lg border border-line px-3 py-1 text-[11.5px]"
|
onClick={(e) => { e.stopPropagation(); navigate(`/inventory?warehouse=${w.id}`); }}
|
||||||
onClick={() => { setEditId(w.id); setEditName(w.name); setEditDesc(w.description || ""); }}
|
>
|
||||||
>
|
📊 مشاهده داشبورد انبار
|
||||||
✏️ ویرایش
|
</button>
|
||||||
</button>
|
{isPM && (
|
||||||
<button className="rounded-lg border border-red/30 px-3 py-1 text-[11.5px] text-red" onClick={() => void removeWarehouse(w.id)}>
|
<>
|
||||||
✕ حذف
|
<button
|
||||||
</button>
|
className="rounded-lg border border-line px-3 py-1 text-[11.5px]"
|
||||||
</div>
|
onClick={(e) => { e.stopPropagation(); setEditId(w.id); setEditName(w.name); setEditDesc(w.description || ""); }}
|
||||||
)}
|
>
|
||||||
|
✏️ ویرایش
|
||||||
|
</button>
|
||||||
|
<button className="rounded-lg border border-red/30 px-3 py-1 text-[11.5px] text-red" onClick={(e) => { e.stopPropagation(); void removeWarehouse(w.id); }}>
|
||||||
|
✕ حذف
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{editId === w.id && isPM && (
|
{editId === w.id && isPM && (
|
||||||
<div className="mt-2 flex flex-col gap-1.5 rounded-lg border border-dashed border-accent/40 p-2">
|
<div className="mt-2 flex flex-col gap-1.5 rounded-lg border border-dashed border-accent/40 p-2" onClick={(e) => e.stopPropagation()}>
|
||||||
<input
|
<input
|
||||||
className="h-8 rounded-lg border border-line bg-panel px-2 text-sm"
|
className="h-8 rounded-lg border border-line bg-panel px-2 text-sm"
|
||||||
placeholder="نام انبار"
|
placeholder="نام انبار"
|
||||||
|
|||||||
@@ -9,22 +9,41 @@ export function useLogout() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// منوی همبرگریِ مختصر برای مدیر تدارکات/ادمین: بخشها بالای صفحه هستند و
|
||||||
|
// اینجا فقط «بازگشت به دفتر» و «خروج از حساب» میماند.
|
||||||
|
export function accountMenuItems(onLogout?: () => void): MenuItem[] {
|
||||||
|
const items: MenuItem[] = [{ label: "بازگشت به دفتر", to: "/" }];
|
||||||
|
if (onLogout) {
|
||||||
|
items.push({ divider: true });
|
||||||
|
items.push({ label: "خروج از حساب", onClick: onLogout });
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
// آیتمهای منوی همبرگری پنل مدیریت انبار — با نشانگر پنل فعلی + «بازگشت به دفتر» + خروج
|
// آیتمهای منوی همبرگری پنل مدیریت انبار — با نشانگر پنل فعلی + «بازگشت به دفتر» + خروج
|
||||||
|
//
|
||||||
|
// دسترسیها (v2.6):
|
||||||
|
// - ادمین / مدیر تدارکات (یک نفر): داشبورد، کنترل روزانه، کالاهای در راه، سازماندهی —
|
||||||
|
// بدون «تغییر موجودی» و بدون «انبارها».
|
||||||
|
// - مدیر پروژه: داشبورد، تغییر موجودی، کنترل روزانه، انبارها، کالاهای در راه —
|
||||||
|
// بدون «سازماندهی».
|
||||||
export function panelMenuItems(pathname: string, role: string | null | undefined, onLogout?: () => void): MenuItem[] {
|
export function panelMenuItems(pathname: string, role: string | null | undefined, onLogout?: () => void): MenuItem[] {
|
||||||
const isAdmin = role === "admin" || role === "procurement_manager";
|
const isAdmin = role === "admin" || role === "procurement_manager";
|
||||||
const isPM = role === "pm";
|
const isPM = role === "pm";
|
||||||
const items: MenuItem[] = [
|
const items: MenuItem[] = [
|
||||||
{ label: "داشبورد انبار", to: "/inventory", active: pathname === "/inventory" },
|
{ label: "داشبورد انبار", to: "/inventory", active: pathname === "/inventory" },
|
||||||
{ label: "تغییر موجودی", to: "/inventory/change", active: pathname.startsWith("/inventory/change") },
|
|
||||||
{ label: "کنترل روزانه", to: "/inventory/daily", active: pathname.startsWith("/inventory/daily") },
|
|
||||||
{ label: "انبارها", to: "/inventory/warehouses", active: pathname.startsWith("/inventory/warehouses") },
|
|
||||||
];
|
];
|
||||||
|
if (isPM) {
|
||||||
|
items.push({ label: "تغییر موجودی", to: "/inventory/change", active: pathname.startsWith("/inventory/change") });
|
||||||
|
}
|
||||||
|
items.push({ label: "کنترل روزانه", to: "/inventory/daily", active: pathname.startsWith("/inventory/daily") });
|
||||||
|
if (isPM) {
|
||||||
|
items.push({ label: "انبارها", to: "/inventory/warehouses", active: pathname.startsWith("/inventory/warehouses") });
|
||||||
|
}
|
||||||
|
items.push({ label: "کالاهای در راه", to: "/inventory/arrivals", active: pathname.startsWith("/inventory/arrivals") });
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
items.push({ label: "سازماندهی", to: "/inventory/org", active: pathname.startsWith("/inventory/org") });
|
items.push({ label: "سازماندهی", to: "/inventory/org", active: pathname.startsWith("/inventory/org") });
|
||||||
}
|
}
|
||||||
if (isPM) {
|
|
||||||
items.push({ label: "رسید کالاهای در راه", to: "/inventory/arrivals", active: pathname.startsWith("/inventory/arrivals") });
|
|
||||||
}
|
|
||||||
items.push({ divider: true });
|
items.push({ divider: true });
|
||||||
items.push({ label: "بازگشت به دفتر", to: "/" });
|
items.push({ label: "بازگشت به دفتر", to: "/" });
|
||||||
if (onLogout) {
|
if (onLogout) {
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ export function useInventoryData(): InventoryData {
|
|||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
const scopedWarehouses = useMemo(
|
const scopedWarehouses = useMemo(
|
||||||
() => (user?.role === "admin" ? warehouses : warehouses.filter((w) => String(w.project_id) === String(user?.project_id || ""))),
|
() =>
|
||||||
|
user?.role === "admin" || user?.role === "procurement_manager"
|
||||||
|
? warehouses
|
||||||
|
: warehouses.filter((w) => String(w.project_id) === String(user?.project_id || "")),
|
||||||
[warehouses, user?.role, user?.project_id],
|
[warehouses, user?.role, user?.project_id],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user