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:
ali_mohaghegh
2026-09-03 15:00:58 +03:30
parent f9ec68815e
commit e4ae34ddbc
9 changed files with 122 additions and 43 deletions
+54
View File
@@ -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(",")]);
}
+15
View File
@@ -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,
},
},
});