feat(ui): AppSwitcherMenu parity + hub-only warehouse headers + calendar badge removal

- shared AppSwitcherMenu ('پنل‌ها' title, active filled item with ✓ فعلی,
  «مدیریت انبار»/«مدیریت تدارکات» switcher, divider + خروج از حساب) used by
  both the procurement dashboard and the warehouse hub (PM variant keeps
  logout-only); replaced the old raw «بازگشت به دفتر» link
- warehouse header mirrors the procurement pattern: icon + hamburger only
  on the hub dashboard; sub-views (تغییر موجودی، کنترل روزانه، انبارها،
  کالاهای در راه، سازمان‌دهی) render a minimal header with just the title
  and «بازگشت به داشبورد»
- removed the self-referencing «داشبورد انبار» tab from the hub strip
- removed the red per-day transaction-count badges from the daily-control
  calendar (backend untouched — purely visual)
- removed the test subtitle «داده‌ها در پایگاه داده ذخیره می‌شوند» from the
  procurement dashboard header
This commit is contained in:
ali_mohaghegh
2026-09-03 15:01:07 +03:30
parent e4ae34ddbc
commit 0cd4f8f361
7 changed files with 53 additions and 61 deletions
@@ -0,0 +1,29 @@
import { PanelMenu } from "./PanelMenu";
import { useLogout } from "../pages/Inventory/panelMenu";
// منوی همبرگری مشترک بین دو ماژول (پنل انبار / دفتر تدارکات) —
// دقیقاً مثل منوی دفتر: عنوان «پنل‌ها»، ماژول فعلی با استایل فعال (پر)
// + «✓ فعلی»، ماژول دیگر به‌عنوان سوییچر، و در پایان جداکننده + خروج.
export function AppSwitcherMenu({
activeModule,
canSwitchToProcurement = true,
}: {
activeModule: "warehouse" | "procurement";
canSwitchToProcurement?: boolean; // مدیر پروژه به دفتر تدارکات دسترسی ندارد
}) {
const logout = useLogout();
const items = [];
if (activeModule === "procurement") {
items.push({ label: "مدیریت انبار", to: "/inventory" });
items.push({ label: "مدیریت تدارکات", to: "/", active: true });
} else {
items.push({ label: "مدیریت انبار", to: "/inventory", active: true });
if (canSwitchToProcurement) {
items.push({ label: "مدیریت تدارکات", to: "/" });
}
}
items.push({ divider: true });
items.push({ label: "خروج از حساب", onClick: logout });
return <PanelMenu title="پنل‌ها" items={items} />;
}
+7 -5
View File
@@ -5,9 +5,10 @@ import { toast } from "sonner";
import { Header, PageLayout } from "../../components/layout/Header";
import { EmptyState } from "../../components/EmptyState";
import { useAuthStore } from "../../store/auth";
import { PanelMenu } from "../../components/PanelMenu";
import { accountMenuItems, inventorySubheader, useLogout } from "../Inventory/panelMenu";
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> = {
@@ -21,7 +22,6 @@ const ROLE_LABELS: Record<string, string> = {
// (مدیریت پروژه‌ها در «سازمان‌دهی» توسط مدیر تدارکات انجام می‌شود).
export function AdminPanel() {
const navigate = useNavigate();
const logout = useLogout();
const user = useAuthStore((s) => s.user);
const [users, setUsers] = useState<UserRow[]>([]);
@@ -53,6 +53,7 @@ export function AdminPanel() {
toast.success(
created > 0 ? `همگام‌سازی انجام شد — ${faNum(created)} کاربر جدید از Casdoor اضافه شد ✓` : "همگام‌سازی انجام شد — کاربر جدیدی یافت نشد",
);
refreshProcurementStore();
} catch (err) {
toast.error(err instanceof Error ? err.message : "خطا در همگام‌سازی با Casdoor");
} finally {
@@ -71,6 +72,7 @@ export function AdminPanel() {
: `نقش مدیر تدارکات از «${u.first_name} ${u.last_name}» گرفته شد`,
);
await load();
refreshProcurementStore();
} catch (err) {
toast.error(err instanceof Error ? err.message : "خطا در تغییر نقش");
} finally {
@@ -81,7 +83,7 @@ export function AdminPanel() {
if (!isAdmin) {
return (
<div className="min-h-screen">
<Header title="پنل مدیریت ادمین" menu={<PanelMenu title="پنل مدیریت انبار" items={accountMenuItems(logout)} />} />
<Header title="پنل مدیریت ادمین" backTo="/inventory" backLabel="بازگشت به داشبورد انبار" />
<PageLayout>
<EmptyState emoji="🔒" text="این پنل فقط برای ادمین (سوپر یوزر) است." />
</PageLayout>
@@ -97,7 +99,7 @@ export function AdminPanel() {
backTo="/inventory"
backLabel="بازگشت به داشبورد انبار"
icon={<ShieldCheck className="h-4.5 w-4.5" strokeWidth={2} />}
menu={<PanelMenu title="پنل مدیریت انبار" items={accountMenuItems(logout)} />}
menu={<AppSwitcherMenu activeModule="warehouse" />}
actions={
<button
onClick={() => void sync()}
+2 -15
View File
@@ -16,8 +16,7 @@ import { Header, PageLayout } from "../../components/layout/Header";
import { StatsBar } from "../../components/StatsBar";
import { FilterToolbar, type Filters } from "./FilterToolbar";
import { LedgerTable } from "./LedgerTable";
import { PanelMenu } from "../../components/PanelMenu";
import { useLogout } from "../Inventory/panelMenu";
import { AppSwitcherMenu } from "../../components/AppSwitcherMenu";
import { AcceptModal } from "./AcceptModal";
import { EditModal } from "./EditModal";
@@ -26,7 +25,6 @@ const EMPTY_FILTERS: Filters = { category: "", supplier: "", status: "", search:
// صفحه‌ی اصلی دفتر پیشنهادهای خرید
export function DashboardPage() {
useStoreData();
const logout = useLogout();
const quotes = useQuotes();
const suppliers = useSuppliers();
@@ -78,19 +76,8 @@ export function DashboardPage() {
<div className="min-h-screen">
<Header
title="دفتر پیشنهادهای خرید"
sub="داده‌ها در پایگاه داده ذخیره می‌شوند"
brand
menu={
<PanelMenu
title="پنل‌ها"
items={[
{ label: "مدیریت انبار", to: "/inventory" },
{ label: "مدیریت تدارکات", to: "/", active: true },
{ divider: true },
{ label: "خروج از حساب", onClick: logout },
]}
/>
}
menu={<AppSwitcherMenu activeModule="procurement" />}
actions={
<>
<Link to="/suppliers" className="btn-line-file no-underline">
@@ -1,10 +1,8 @@
import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Warehouse as WarehouseIcon } from "lucide-react";
import { Header, PageLayout } from "../../components/layout/Header";
import { useAuthStore } from "../../store/auth";
import { PanelMenu } from "../../components/PanelMenu";
import { accountMenuItems, inventorySubheader, pmMenuItems, useLogout } from "./panelMenu";
import { inventorySubheader } from "./panelMenu";
import { useInventoryData, unitName, whName } from "./useInventoryData";
import { faNum } from "../../lib/formatters";
import { gregorianToJalali, jalaliToGregorian, MONTHS, toFa } from "../../lib/jalali";
@@ -20,7 +18,6 @@ function todayKey(): string {
// کنترل روزانه — پورت کامل تقویم شمسی نسخه‌ی ارسالی
export function DailyControlPage() {
const logout = useLogout();
const user = useAuthStore((s) => s.user);
const data = useInventoryData();
const navigate = useNavigate();
@@ -124,8 +121,6 @@ export function DailyControlPage() {
sub={inventorySubheader(user)}
backTo="/inventory"
backLabel="بازگشت به داشبورد"
icon={<WarehouseIcon className="h-4.5 w-4.5" strokeWidth={2} />}
menu={<PanelMenu title="پنل مدیریت انبار" items={isAdmin ? accountMenuItems(logout) : pmMenuItems(logout)} />}
/>
<PageLayout className="flex flex-col gap-3">
{isAdmin && (
@@ -175,11 +170,6 @@ export function DailyControlPage() {
className={`relative rounded-lg py-2 text-[12px] ${!c.inMonth ? "text-muted/40" : ""} ${c.isToday ? "bg-accent font-bold text-white" : ""} ${sel === k ? "ring-2 ring-accent" : "hover:bg-accent/5"}`}
>
{toFa(jd)}
{c.n > 0 && (
<span className="absolute -top-0.5 -left-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-red text-[9px] font-bold text-white">
{toFa(c.n)}
</span>
)}
</button>
);
})}
+4 -10
View File
@@ -5,8 +5,8 @@ import { Header, PageLayout } from "../../components/layout/Header";
import { EmptyState } from "../../components/EmptyState";
import { Badge } from "../../components/ui/badge";
import { useAuthStore } from "../../store/auth";
import { PanelMenu } from "../../components/PanelMenu";
import { accountMenuItems, inventorySubheader, panelMenuItems, pmMenuItems, useLogout } from "./panelMenu";
import { AppSwitcherMenu } from "../../components/AppSwitcherMenu";
import { inventorySubheader } from "./panelMenu";
import { InventoryTopNav } from "./InventoryTopNav";
import { useInventoryData, brandName, catName, materialName, unitName, whName } from "./useInventoryData";
import type { InventoryRow, InventoryTransaction, TransactionType } from "../../lib/types";
@@ -50,7 +50,6 @@ function specsHash(specs: Record<string, string> | null | undefined): string {
// داشبورد انبار — پورت کامل نسخه‌ی ارسالی (گروه‌بندی + خط زمانی + فیلترها)
export function InventoryPage() {
const location = useLocation();
const logout = useLogout();
const user = useAuthStore((s) => s.user);
const { warehouses, inventory, transactions, lookup, loading } = useInventoryData();
const isAdmin = user?.role === "admin" || user?.role === "procurement_manager"; // مدیر تدارکات = ادمین دفتر
@@ -127,7 +126,7 @@ export function InventoryPage() {
if (!user || (!isAdmin && !isPM)) {
return (
<div className="min-h-screen">
<Header title="داشبورد انبار" menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />} />
<Header title="داشبورد انبار" backTo="/inventory" backLabel="بازگشت به داشبورد" />
<PageLayout>
<EmptyState emoji="🔒" text="دسترسی به انبار فقط برای مدیر پروژه یا ادمین است." />
</PageLayout>
@@ -141,12 +140,7 @@ export function InventoryPage() {
title="سامانه مدیریت انبار"
sub={inventorySubheader(user)}
icon={<WarehouseIcon className="h-4.5 w-4.5" strokeWidth={2} />}
menu={
<PanelMenu
title="پنل مدیریت انبار"
items={isAdmin ? accountMenuItems(logout) : pmMenuItems(logout)}
/>
}
menu={<AppSwitcherMenu activeModule="warehouse" canSwitchToProcurement={isAdmin} />}
actions={
<>
<InventoryTopNav pathname={location.pathname} variant={isAdmin ? "admin" : "pm"} />
@@ -2,14 +2,12 @@ import { Link } from "react-router-dom";
import { cn } from "../../lib/utils";
// ناوبری بالای صفحه در پنل انبار — فقط روی «داشبورد انبار» (هاب) دیده می‌شود؛
// زیرصفحه‌ها تب ندارند و فقط با «بازگشت به داشبورد» به هاب برمی‌گردند.
// خودِ «داشبورد انبار» دکمه ندارد (ریشه/هاب است) و زیرصفحه‌ها تب ندارند و
// فقط با «بازگشت به داشبورد» به هاب برمی‌گردند.
//
// - variant="pm" (مدیر پروژه): همه بخش‌ها — بدون «تغییر موجودی» (ورود آن از
// دکمه‌ی «تغییر موجودی» بالای هاب است)، بدون «دفتر تدارکات» (مدیر پروژه
// به سیستم تدارکات دسترسی ندارد) و بدون دکمه‌ی خروج تکی (خروج از حساب
// فقط در منوی همبرگری است).
// - variant="admin" (مدیر تدارکات/ادمین): فقط بخش‌های مدیر تدارکات —
// «بازگشت به دفتر» و «خروج از حساب» در منوی همبرگری می‌مانند.
// - variant="pm" (مدیر پروژه): کنترل روزانه، انبارها، کالاهای در راه —
// ورود به «تغییر موجودی» از دکمه‌ی بالای هاب است.
// - variant="admin" (مدیر تدارکات/ادمین): کنترل روزانه، کالاهای در راه، سازمان‌دهی.
export function InventoryTopNav({
pathname,
variant = "pm",
@@ -20,13 +18,11 @@ export function InventoryTopNav({
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/daily", active: pathname.startsWith("/inventory/daily") },
{ label: "انبارها", to: "/inventory/warehouses", active: pathname.startsWith("/inventory/warehouses") },
{ label: "کالاهای در راه", to: "/inventory/arrivals", active: pathname.startsWith("/inventory/arrivals") },
@@ -1,11 +1,9 @@
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { Warehouse as WarehouseIcon } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { Header, PageLayout } from "../../components/layout/Header";
import { useAuthStore } from "../../store/auth";
import { PanelMenu } from "../../components/PanelMenu";
import { inventorySubheader, panelMenuItems, pmMenuItems, useLogout } from "./panelMenu";
import { inventorySubheader } from "./panelMenu";
import { apiV26 } from "../../lib/api";
import { useInventoryData, projName } from "./useInventoryData";
import { faNum } from "../../lib/formatters";
@@ -13,9 +11,7 @@ import { isoToJalaliStr } from "../../lib/jalali";
// انبارها — مدیر پروژه: مشاهده انبارهای پروژه خودش + افزودن/ویرایش/حذف (ادمین دسترسی ندارد)
export function WarehousesPage() {
const location = useLocation();
const navigate = useNavigate();
const logout = useLogout();
const user = useAuthStore((s) => s.user);
const data = useInventoryData();
const isAdmin = user?.role === "admin" || user?.role === "procurement_manager";
@@ -31,7 +27,7 @@ export function WarehousesPage() {
if (isAdmin) {
return (
<div className="min-h-screen">
<Header title="انبارها" menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />} />
<Header title="انبارها" backTo="/inventory" backLabel="بازگشت به داشبورد" />
<PageLayout>
<div className="rounded-xl border border-line bg-panel p-8 text-center">
<div className="text-3xl">🔒</div>
@@ -47,7 +43,7 @@ export function WarehousesPage() {
if (!isPM) {
return (
<div className="min-h-screen">
<Header title="انبارها" menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />} />
<Header title="انبارها" backTo="/inventory" backLabel="بازگشت به داشبورد" />
<PageLayout>
<div className="text-[13px] text-muted">این بخش فقط برای مدیر پروژه است.</div>
</PageLayout>
@@ -101,8 +97,6 @@ export function WarehousesPage() {
sub={inventorySubheader(user)}
backTo="/inventory"
backLabel="بازگشت به داشبورد"
icon={<WarehouseIcon className="h-4.5 w-4.5" strokeWidth={2} />}
menu={<PanelMenu title="پنل مدیریت انبار" items={pmMenuItems(logout)} />}
/>
<PageLayout className="flex flex-col gap-3">
{isPM && (