feat(sync): live cross-module cache invalidation (no manual refresh)
- procurement store refreshes right after a warehouse receipt (arrivals) and after role/project changes (organization/admin), so the ledger reflects warehouse actions immediately - refetchOnWindowFocus enabled on the shared query client - BroadcastChannel (lib/crossTab.ts): mutations broadcast 'quotes' / 'inventory'; all warehouse pages + the store hook resync open tabs automatically (receipt in one tab updates the ledger in another) - warehouse mutations (stock changes) notify other tabs; same-page refresh behavior unchanged
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
export { useStoreData, useInvalidateStore } from "./useStoreData";
|
export { useStoreData, useInvalidateStore, refreshProcurementStore } from "./useStoreData";
|
||||||
export { useQuotes, useCreateQuote, useUpdateQuote, useDeleteQuote } from "./useQuotes";
|
export { useQuotes, useCreateQuote, useUpdateQuote, useDeleteQuote } from "./useQuotes";
|
||||||
export { useSuppliers, useAddSupplier, useUpdateSupplier, useRemoveSupplier } from "./useSuppliers";
|
export { useSuppliers, useAddSupplier, useUpdateSupplier, useRemoveSupplier } from "./useSuppliers";
|
||||||
export { useMaterials, useAddMaterial } from "./useMaterials";
|
export { useMaterials, useAddMaterial } from "./useMaterials";
|
||||||
|
|||||||
@@ -4,12 +4,16 @@
|
|||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { useAppStore } from "../store";
|
import { useAppStore } from "../store";
|
||||||
|
import { notifyDataChanged, useDataChanged } from "../lib/crossTab";
|
||||||
|
import { queryClient } from "../lib/queryClient";
|
||||||
|
|
||||||
export const STORE_KEY = ["store"] as const;
|
export const STORE_KEY = ["store"] as const;
|
||||||
|
|
||||||
|
export { queryClient };
|
||||||
|
|
||||||
// یک بار در ریشهی برنامه صدا زده میشود: لود + mirror در Zustand
|
// یک بار در ریشهی برنامه صدا زده میشود: لود + mirror در Zustand
|
||||||
export function useStoreData(): void {
|
export function useStoreData(): void {
|
||||||
const setData = useAppStore((s) => s.setData);
|
const setData = useAppStore((s) => s.setData);
|
||||||
@@ -21,12 +25,25 @@ export function useStoreData(): void {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) setData(data);
|
if (data) setData(data);
|
||||||
}, [data, setData]);
|
}, [data, setData]);
|
||||||
|
|
||||||
|
// تبهای دیگر (رسید انبار / تغییر نقش و...) → بازخوانی خودکار store
|
||||||
|
useDataChanged(
|
||||||
|
useCallback(() => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: STORE_KEY });
|
||||||
|
}, []),
|
||||||
|
["quotes"],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// بعد از هر mutation برای همگرایی با حقیقت
|
// بعد از هر mutation برای همگرایی با حقیقت (+ اعلام به تبهای دیگر)
|
||||||
export function useInvalidateStore(): () => void {
|
export function useInvalidateStore(): () => void {
|
||||||
const qc = useQueryClient();
|
return useCallback(refreshProcurementStore, []);
|
||||||
return useCallback(() => {
|
}
|
||||||
void qc.invalidateQueries({ queryKey: STORE_KEY });
|
|
||||||
}, [qc]);
|
// نسخهی تابعی (خارج از کامپوننت هم قابل صدا زدن) — بعد از تغییر دادههای
|
||||||
|
// مرتبط با پیشنهادها (مثلاً رسید انبار) خودِ store را تازه میکند تا دفتر
|
||||||
|
// بدون refresh دستی بهروز شود؛ و به تبهای دیگر هم اعلام میکند.
|
||||||
|
export function refreshProcurementStore(): void {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: STORE_KEY });
|
||||||
|
notifyDataChanged("quotes");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// ============================================================
|
||||||
|
// همگامسازی بین تبها (BroadcastChannel) — بعد از هر mutation که روی
|
||||||
|
// دادههای «پیشنهادها» یا «انبار» اثر میگذارد، به تبهای دیگر اعلام میشود
|
||||||
|
// تا آنها هم دادههای خود را تازه کنند (بدون نیاز به refresh دستی).
|
||||||
|
// BroadcastChannel در خودِ همان تب پیام را نمیدهد — همتبهها همانجا
|
||||||
|
// refresh میشوند و این فقط برای تبهای دیگر است.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
const CHANNEL = "procurement-data-sync";
|
||||||
|
|
||||||
|
export type DataScope = "quotes" | "inventory";
|
||||||
|
|
||||||
|
let _channel: BroadcastChannel | null = null;
|
||||||
|
|
||||||
|
function channel(): BroadcastChannel | null {
|
||||||
|
if (typeof BroadcastChannel === "undefined") return null;
|
||||||
|
if (!_channel) {
|
||||||
|
try {
|
||||||
|
_channel = new BroadcastChannel(CHANNEL);
|
||||||
|
} catch {
|
||||||
|
_channel = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** بعد از تغییر داده در این تب، به بقیهی تبها اعلام کن. */
|
||||||
|
export function notifyDataChanged(scope: DataScope): void {
|
||||||
|
const ch = channel();
|
||||||
|
if (!ch) return;
|
||||||
|
try {
|
||||||
|
ch.postMessage({ scope });
|
||||||
|
} catch {
|
||||||
|
/* نادیده — پیام فقط بهروزرسانی است */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** گوش دادن به تغییرات تبهای دیگر (scope دلخواه یا همه). */
|
||||||
|
export function useDataChanged(onEvent: (scope: DataScope) => void, scopes?: DataScope[]): void {
|
||||||
|
useEffect(() => {
|
||||||
|
const ch = channel();
|
||||||
|
if (!ch) return;
|
||||||
|
const handler = (ev: MessageEvent) => {
|
||||||
|
const scope = (ev.data as { scope?: DataScope } | null)?.scope;
|
||||||
|
if (!scope) return;
|
||||||
|
if (scopes && !scopes.includes(scope)) return;
|
||||||
|
onEvent(scope);
|
||||||
|
};
|
||||||
|
ch.addEventListener("message", handler);
|
||||||
|
return () => ch.removeEventListener("message", handler);
|
||||||
|
}, [onEvent, scopes?.join(",")]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// ============================================================
|
||||||
|
// TanStack Query client — مشترک در کل برنامه (و helperهای سوییچر ماژولها)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: false,
|
||||||
|
// بازخوانی هنگام بازگشت به تب — تا دادههای ماژول دیگر تازه بماند
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
+2
-10
@@ -1,19 +1,11 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import "./styles/globals.css";
|
import "./styles/globals.css";
|
||||||
|
import { queryClient } from "./lib/queryClient";
|
||||||
const queryClient = new QueryClient({
|
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
retry: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
|
||||||
import { Warehouse as WarehouseIcon } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
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 { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import { useAuthStore } from "../../store/auth";
|
import { useAuthStore } from "../../store/auth";
|
||||||
import { PanelMenu } from "../../components/PanelMenu";
|
import { inventorySubheader } from "./panelMenu";
|
||||||
import { accountMenuItems, inventorySubheader, panelMenuItems, pmMenuItems, useLogout } from "./panelMenu";
|
|
||||||
import { apiV26 } from "../../lib/api";
|
import { apiV26 } from "../../lib/api";
|
||||||
|
import { refreshProcurementStore } from "../../hooks";
|
||||||
import type { PendingArrival, Warehouse } from "../../lib/types";
|
import type { PendingArrival, Warehouse } from "../../lib/types";
|
||||||
import { faNum, fmtDate } from "../../lib/formatters";
|
import { faNum, fmtDate } from "../../lib/formatters";
|
||||||
|
|
||||||
@@ -22,8 +20,6 @@ interface AllocRow { key: number; qty: string; whId: string; }
|
|||||||
interface ItemDraft { received: string; allocs: AllocRow[]; }
|
interface ItemDraft { received: string; allocs: AllocRow[]; }
|
||||||
|
|
||||||
export function ArrivalsPage() {
|
export function ArrivalsPage() {
|
||||||
const location = useLocation();
|
|
||||||
const logout = useLogout();
|
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
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";
|
||||||
@@ -193,6 +189,9 @@ export function ArrivalsPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
toast.success("رسید (کامل یا جزیی) ثبت شد و به انبارها وارد شد ✓ — اقلام باقیمانده همچنان در راه هستند");
|
toast.success("رسید (کامل یا جزیی) ثبت شد و به انبارها وارد شد ✓ — اقلام باقیمانده همچنان در راه هستند");
|
||||||
|
// همگامسازی با دفتر تدارکات: store پیشنهادها را تازه کن تا کارتهای
|
||||||
|
// پیشنهاد (بدون refresh دستی) رسید را نشان دهند.
|
||||||
|
refreshProcurementStore();
|
||||||
setDrafts((d) => {
|
setDrafts((d) => {
|
||||||
const next = { ...d };
|
const next = { ...d };
|
||||||
submittable.forEach((it) => delete next[it.item_decision_id]);
|
submittable.forEach((it) => delete next[it.item_decision_id]);
|
||||||
@@ -214,7 +213,7 @@ export function ArrivalsPage() {
|
|||||||
if (!isAdmin && !isPM) {
|
if (!isAdmin && !isPM) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Header title="کالاهای در راه" menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />} />
|
<Header title="کالاهای در راه" backTo="/inventory" backLabel="بازگشت به داشبورد" />
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<EmptyState emoji="🔒" text="این بخش فقط برای مدیر پروژه و مدیر تدارکات است." />
|
<EmptyState emoji="🔒" text="این بخش فقط برای مدیر پروژه و مدیر تدارکات است." />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
@@ -229,8 +228,6 @@ export function ArrivalsPage() {
|
|||||||
sub={inventorySubheader(user)}
|
sub={inventorySubheader(user)}
|
||||||
backTo="/inventory"
|
backTo="/inventory"
|
||||||
backLabel="بازگشت به داشبورد"
|
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-4">
|
<PageLayout className="flex flex-col gap-4">
|
||||||
<div className="rounded-lg border border-blue/20 bg-blue/5 px-3 py-2 text-[12px] leading-6 text-muted">
|
<div className="rounded-lg border border-blue/20 bg-blue/5 px-3 py-2 text-[12px] leading-6 text-muted">
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useLocation, useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { Warehouse as WarehouseIcon } from "lucide-react";
|
|
||||||
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 { inventorySubheader } from "./panelMenu";
|
||||||
import { inventorySubheader, panelMenuItems, pmMenuItems, useLogout } from "./panelMenu";
|
|
||||||
import { apiV26 } from "../../lib/api";
|
import { apiV26 } from "../../lib/api";
|
||||||
|
import { notifyDataChanged } from "../../lib/crossTab";
|
||||||
import { useInventoryData, unitName, whName } from "./useInventoryData";
|
import { useInventoryData, unitName, whName } from "./useInventoryData";
|
||||||
import { JalaliDatePicker } from "../../components/JalaliDatePicker";
|
import { JalaliDatePicker } from "../../components/JalaliDatePicker";
|
||||||
import type { InventoryTransaction, TransactionType } from "../../lib/types";
|
import type { InventoryTransaction, TransactionType } from "../../lib/types";
|
||||||
@@ -46,8 +45,6 @@ function jalDateLabel(d: Date): string {
|
|||||||
|
|
||||||
// تغییر موجودی — پورت کامل صفحهی ارسالی (همه انواع تراکنش)
|
// تغییر موجودی — پورت کامل صفحهی ارسالی (همه انواع تراکنش)
|
||||||
export function ChangeInventoryPage() {
|
export function ChangeInventoryPage() {
|
||||||
const location = useLocation();
|
|
||||||
const logout = useLogout();
|
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
const data = useInventoryData();
|
const data = useInventoryData();
|
||||||
const [params] = useSearchParams();
|
const [params] = useSearchParams();
|
||||||
@@ -186,6 +183,8 @@ export function ChangeInventoryPage() {
|
|||||||
try {
|
try {
|
||||||
for (const op of ops) await postOne(op);
|
for (const op of ops) await postOne(op);
|
||||||
toast.success("ثبت شد ✓");
|
toast.success("ثبت شد ✓");
|
||||||
|
// اعلام به تبهای دیگر (داشبورد/کنترل روزانه/... باز خودشان را تازه میکنند)
|
||||||
|
notifyDataChanged("inventory");
|
||||||
setRows([]);
|
setRows([]);
|
||||||
setMatTxt("");
|
setMatTxt("");
|
||||||
setBrandTxt("");
|
setBrandTxt("");
|
||||||
@@ -394,7 +393,7 @@ export function ChangeInventoryPage() {
|
|||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Header title="تغییر موجودی" menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />} />
|
<Header title="تغییر موجودی" backTo="/inventory" backLabel="بازگشت به داشبورد" />
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="rounded-xl border border-line bg-panel p-8 text-center">
|
<div className="rounded-xl border border-line bg-panel p-8 text-center">
|
||||||
<div className="text-3xl">🔒</div>
|
<div className="text-3xl">🔒</div>
|
||||||
@@ -408,7 +407,7 @@ export function ChangeInventoryPage() {
|
|||||||
if (!isPM) {
|
if (!isPM) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Header title="تغییر موجودی" menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />} />
|
<Header title="تغییر موجودی" backTo="/inventory" backLabel="بازگشت به داشبورد" />
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="text-[13px] text-muted">این بخش فقط برای مدیر پروژه است.</div>
|
<div className="text-[13px] text-muted">این بخش فقط برای مدیر پروژه است.</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
@@ -426,8 +425,6 @@ export function ChangeInventoryPage() {
|
|||||||
sub={inventorySubheader(user)}
|
sub={inventorySubheader(user)}
|
||||||
backTo="/inventory"
|
backTo="/inventory"
|
||||||
backLabel="بازگشت به داشبورد"
|
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">
|
<PageLayout className="flex flex-col gap-3">
|
||||||
{/* انتخاب نوع تراکنش */}
|
{/* انتخاب نوع تراکنش */}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Warehouse as WarehouseIcon } from "lucide-react";
|
|
||||||
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 { inventorySubheader } from "./panelMenu";
|
||||||
import { accountMenuItems, inventorySubheader, useLogout } from "./panelMenu";
|
|
||||||
import { apiV26, type UserRow } from "../../lib/api";
|
import { apiV26, type UserRow } from "../../lib/api";
|
||||||
|
import { refreshProcurementStore } from "../../hooks";
|
||||||
import { useInventoryData } from "./useInventoryData";
|
import { useInventoryData } from "./useInventoryData";
|
||||||
import { faNum } from "../../lib/formatters";
|
import { faNum } from "../../lib/formatters";
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@ const ROLE_LABELS: Record<string, string> = {
|
|||||||
// سازماندهی (فقط ادمین / مدیر تدارکات) — کاربران، مدیران پروژه و انبارهای هر پروژه
|
// سازماندهی (فقط ادمین / مدیر تدارکات) — کاربران، مدیران پروژه و انبارهای هر پروژه
|
||||||
export function OrganizationPage() {
|
export function OrganizationPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const logout = useLogout();
|
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
const data = useInventoryData();
|
const data = useInventoryData();
|
||||||
|
|
||||||
@@ -74,7 +72,7 @@ export function OrganizationPage() {
|
|||||||
if (!canManage) {
|
if (!canManage) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Header title="سازماندهی" menu={<PanelMenu title="پنل مدیریت انبار" items={accountMenuItems(logout)} />} />
|
<Header title="سازماندهی" backTo="/inventory" backLabel="بازگشت به داشبورد" />
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="text-[13px] text-muted">این بخش فقط برای ادمین / مدیر تدارکات است.</div>
|
<div className="text-[13px] text-muted">این بخش فقط برای ادمین / مدیر تدارکات است.</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
@@ -90,6 +88,7 @@ export function OrganizationPage() {
|
|||||||
await apiV26.users.updateRole(u.id, role);
|
await apiV26.users.updateRole(u.id, role);
|
||||||
toast.success(`نقش «${ROLE_LABELS[role ?? ""] ?? "بدون نقش"}» برای ${u.first_name} ${u.last_name} ثبت شد ✓`);
|
toast.success(`نقش «${ROLE_LABELS[role ?? ""] ?? "بدون نقش"}» برای ${u.first_name} ${u.last_name} ثبت شد ✓`);
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
|
refreshProcurementStore();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err instanceof Error ? err.message : "خطا در تغییر نقش");
|
toast.error(err instanceof Error ? err.message : "خطا در تغییر نقش");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -105,6 +104,7 @@ export function OrganizationPage() {
|
|||||||
toast.success(project ? `«${u.first_name} ${u.last_name}» به پروژه «${project.name}» متصل شد ✓` : "مدیر پروژه از پروژه جدا شد");
|
toast.success(project ? `«${u.first_name} ${u.last_name}» به پروژه «${project.name}» متصل شد ✓` : "مدیر پروژه از پروژه جدا شد");
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
await data.refresh();
|
await data.refresh();
|
||||||
|
refreshProcurementStore();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err instanceof Error ? err.message : "خطا در اتصال مدیر به پروژه");
|
toast.error(err instanceof Error ? err.message : "خطا در اتصال مدیر به پروژه");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -158,8 +158,6 @@ export function OrganizationPage() {
|
|||||||
sub={inventorySubheader(user)}
|
sub={inventorySubheader(user)}
|
||||||
backTo="/inventory"
|
backTo="/inventory"
|
||||||
backLabel="بازگشت به داشبورد"
|
backLabel="بازگشت به داشبورد"
|
||||||
icon={<WarehouseIcon className="h-4.5 w-4.5" strokeWidth={2} />}
|
|
||||||
menu={<PanelMenu title="پنل مدیریت انبار" items={accountMenuItems(logout)} />}
|
|
||||||
/>
|
/>
|
||||||
<PageLayout className="flex flex-col gap-3">
|
<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 className="rounded-lg border border-blue/20 bg-blue/5 px-3 py-2 text-[12px] leading-6 text-muted">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { apiV26 } from "../../lib/api";
|
import { apiV26 } from "../../lib/api";
|
||||||
import { useAuthStore } from "../../store/auth";
|
import { useAuthStore } from "../../store/auth";
|
||||||
|
import { useDataChanged } from "../../lib/crossTab";
|
||||||
import type { InventoryRow, InventoryTransaction, Warehouse } from "../../lib/types";
|
import type { InventoryRow, InventoryTransaction, Warehouse } from "../../lib/types";
|
||||||
|
|
||||||
export interface LookupData {
|
export interface LookupData {
|
||||||
@@ -63,6 +64,14 @@ export function useInventoryData(): InventoryData {
|
|||||||
void load();
|
void load();
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
|
// تبهای دیگر (ثبت تراکنش/رسید در تب دوم) → بازخوانی خودکار همین صفحه
|
||||||
|
useDataChanged(
|
||||||
|
useCallback(() => {
|
||||||
|
void load();
|
||||||
|
}, [load]),
|
||||||
|
["quotes", "inventory"],
|
||||||
|
);
|
||||||
|
|
||||||
const scopedWarehouses = useMemo(
|
const scopedWarehouses = useMemo(
|
||||||
() =>
|
() =>
|
||||||
user?.role === "admin" || user?.role === "procurement_manager"
|
user?.role === "admin" || user?.role === "procurement_manager"
|
||||||
|
|||||||
Reference in New Issue
Block a user