feat(realtime): implement SSE realtime notifications; publish events from resolvers; remove polling in chat/sidebar/supplies/warehouse and wire realtime refetch
This commit is contained in:
@ -23,12 +23,12 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { GET_CONVERSATIONS, GET_INCOMING_REQUESTS, GET_PENDING_SUPPLIES_COUNT } from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useRealtime } from '@/hooks/useRealtime'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
|
||||
// Компонент для отображения логистических заявок (только для логистики)
|
||||
function LogisticsOrdersNotification() {
|
||||
const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
pollInterval: 30000, // Обновляем каждые 30 секунд
|
||||
const { data: pendingData, refetch: refetchPending } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore',
|
||||
})
|
||||
@ -46,8 +46,7 @@ function LogisticsOrdersNotification() {
|
||||
|
||||
// Компонент для отображения поставок фулфилмента (только поставки, не заявки на партнерство)
|
||||
function FulfillmentSuppliesNotification() {
|
||||
const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
pollInterval: 30000, // Обновляем каждые 30 секунд
|
||||
const { data: pendingData, refetch: refetchPending } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore',
|
||||
})
|
||||
@ -65,8 +64,7 @@ function FulfillmentSuppliesNotification() {
|
||||
|
||||
// Компонент для отображения входящих заказов поставщика (только входящие заказы, не заявки на партнерство)
|
||||
function WholesaleOrdersNotification() {
|
||||
const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
pollInterval: 30000, // Обновляем каждые 30 секунд
|
||||
const { data: pendingData, refetch: refetchPending } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore',
|
||||
})
|
||||
@ -95,21 +93,38 @@ export function Sidebar({ isRootInstance = false }: { isRootInstance?: boolean }
|
||||
const { isCollapsed, toggleSidebar } = useSidebar()
|
||||
|
||||
// Загружаем список чатов для подсчета непрочитанных сообщений
|
||||
const { data: conversationsData } = useQuery(GET_CONVERSATIONS, {
|
||||
pollInterval: 60000, // Обновляем каждую минуту в сайдбаре - этого достаточно
|
||||
const { data: conversationsData, refetch: refetchConversations } = useQuery(GET_CONVERSATIONS, {
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore', // Игнорируем ошибки чтобы не ломать сайдбар
|
||||
notifyOnNetworkStatusChange: false, // Плавные обновления без мерцания
|
||||
})
|
||||
|
||||
// Загружаем входящие заявки для подсчета новых запросов
|
||||
const { data: incomingRequestsData } = useQuery(GET_INCOMING_REQUESTS, {
|
||||
pollInterval: 60000, // Обновляем каждую минуту
|
||||
const { data: incomingRequestsData, refetch: refetchIncoming } = useQuery(GET_INCOMING_REQUESTS, {
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore',
|
||||
notifyOnNetworkStatusChange: false,
|
||||
})
|
||||
|
||||
// Реалтайм обновления бейджей
|
||||
useRealtime({
|
||||
onEvent: (evt) => {
|
||||
switch (evt.type) {
|
||||
case 'message:new':
|
||||
refetchConversations()
|
||||
break
|
||||
case 'counterparty:request:new':
|
||||
case 'counterparty:request:updated':
|
||||
refetchIncoming()
|
||||
break
|
||||
case 'supply-order:new':
|
||||
case 'supply-order:updated':
|
||||
refetchPending()
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Если уже есть корневой сайдбар и это не корневой экземпляр — не рендерим дубликат
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
|
@ -7,6 +7,7 @@ import React, { useState } from 'react'
|
||||
import { Sidebar } from '@/components/dashboard/sidebar'
|
||||
import { GET_PENDING_SUPPLIES_COUNT } from '@/graphql/queries'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
import { useRealtime } from '@/hooks/useRealtime'
|
||||
|
||||
// Импорты компонентов подразделов
|
||||
import { FulfillmentConsumablesOrdersTab } from './fulfillment-supplies/fulfillment-consumables-orders-tab'
|
||||
@ -31,8 +32,7 @@ export function FulfillmentSuppliesDashboard() {
|
||||
const [activeThirdTab, setActiveThirdTab] = useState('new') // новые
|
||||
|
||||
// Загружаем данные о непринятых поставках
|
||||
const { data: pendingData, error: pendingError } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
pollInterval: 30000, // Обновляем каждые 30 секунд
|
||||
const { data: pendingData, error: pendingError, refetch: refetchPending } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore',
|
||||
onError: (error) => {
|
||||
@ -40,6 +40,15 @@ export function FulfillmentSuppliesDashboard() {
|
||||
},
|
||||
})
|
||||
|
||||
// Realtime: обновление бейджа
|
||||
useRealtime({
|
||||
onEvent: (evt) => {
|
||||
if (evt.type === 'supply-order:new' || evt.type === 'supply-order:updated') {
|
||||
refetchPending()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Логируем ошибку для диагностики
|
||||
React.useEffect(() => {
|
||||
if (pendingError) {
|
||||
|
@ -7,6 +7,7 @@ import React, { useState, useEffect } from 'react'
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { GET_PENDING_SUPPLIES_COUNT } from '@/graphql/queries'
|
||||
import { useRealtime } from '@/hooks/useRealtime'
|
||||
|
||||
// Импорты компонентов подкатегорий
|
||||
import { FulfillmentConsumablesOrdersTab } from './fulfillment-consumables-orders-tab'
|
||||
@ -33,8 +34,7 @@ export function FulfillmentSuppliesTab() {
|
||||
const [activeTab, setActiveTab] = useState('goods')
|
||||
|
||||
// Загружаем данные о непринятых поставках
|
||||
const { data: pendingData, error: pendingError } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
pollInterval: 30000, // Обновляем каждые 30 секунд
|
||||
const { data: pendingData, error: pendingError, refetch: refetchPending } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore',
|
||||
onError: (error) => {
|
||||
@ -42,6 +42,15 @@ export function FulfillmentSuppliesTab() {
|
||||
},
|
||||
})
|
||||
|
||||
// Realtime: обновление счетчика
|
||||
useRealtime({
|
||||
onEvent: (evt) => {
|
||||
if (evt.type === 'supply-order:new' || evt.type === 'supply-order:updated') {
|
||||
refetchPending()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Логируем ошибку для диагностики
|
||||
React.useEffect(() => {
|
||||
if (pendingError) {
|
||||
|
@ -46,6 +46,7 @@ import {
|
||||
} from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
import { useRealtime } from '@/hooks/useRealtime'
|
||||
|
||||
import { WbReturnClaims } from './wb-return-claims'
|
||||
|
||||
@ -238,7 +239,26 @@ export function FulfillmentWarehouseDashboard() {
|
||||
refetch: refetchWarehouseStats,
|
||||
} = useQuery(GET_FULFILLMENT_WAREHOUSE_STATS, {
|
||||
fetchPolicy: 'no-cache', // Принудительно обходим кеш
|
||||
pollInterval: 60000, // Обновляем каждую минуту
|
||||
})
|
||||
|
||||
// Real-time: обновляем ключевые блоки при событиях поставок/склада
|
||||
useRealtime({
|
||||
onEvent: (evt) => {
|
||||
switch (evt.type) {
|
||||
case 'supply-order:new':
|
||||
case 'supply-order:updated':
|
||||
refetchOrders()
|
||||
refetchWarehouseStats()
|
||||
refetchProducts()
|
||||
refetchSellerSupplies()
|
||||
refetchFulfillmentSupplies()
|
||||
break
|
||||
case 'warehouse:changed':
|
||||
refetchWarehouseStats()
|
||||
refetchFulfillmentSupplies()
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Логируем статистику склада для отладки
|
||||
|
@ -12,6 +12,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { VoicePlayer } from '@/components/ui/voice-player'
|
||||
import { GET_MESSAGES } from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useRealtime } from '@/hooks/useRealtime'
|
||||
|
||||
interface Organization {
|
||||
id: string
|
||||
@ -50,17 +51,32 @@ export function MessengerAttachments({ counterparty, onViewChange }: MessengerAt
|
||||
const [lightboxImage, setLightboxImage] = useState<{ url: string; fileName: string; fileSize?: number } | null>(null)
|
||||
|
||||
// Загружаем все сообщения для получения вложений
|
||||
const {
|
||||
data: messagesData,
|
||||
loading,
|
||||
refetch,
|
||||
} = useQuery(GET_MESSAGES, {
|
||||
const { data: messagesData, loading, refetch } = useQuery(GET_MESSAGES, {
|
||||
variables: { counterpartyId: counterparty.id, limit: 1000 },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
pollInterval: 5000, // Обновляем каждые 5 секунд
|
||||
notifyOnNetworkStatusChange: false, // Не показываем loading при обновлениях
|
||||
})
|
||||
|
||||
// Реалтайм обновление вложений
|
||||
useRealtime({
|
||||
onEvent: (evt) => {
|
||||
if (evt.type !== 'message:new') return
|
||||
const { senderOrgId, receiverOrgId } = (evt.payload || {}) as {
|
||||
senderOrgId?: string
|
||||
receiverOrgId?: string
|
||||
}
|
||||
if (!user?.organization?.id) return
|
||||
if (
|
||||
senderOrgId === counterparty.id ||
|
||||
receiverOrgId === counterparty.id ||
|
||||
senderOrgId === user.organization.id ||
|
||||
receiverOrgId === user.organization.id
|
||||
) {
|
||||
refetch()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Обновляем данные при открытии вкладки вложений
|
||||
useEffect(() => {
|
||||
onViewChange?.()
|
||||
|
@ -24,6 +24,7 @@ import { GET_MESSAGES, GET_CONVERSATIONS } from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
|
||||
import { MessengerAttachments } from './messenger-attachments'
|
||||
import { useRealtime } from '@/hooks/useRealtime'
|
||||
|
||||
interface Organization {
|
||||
id: string
|
||||
@ -68,13 +69,8 @@ export function MessengerChat({ counterparty, onMessagesRead }: MessengerChatPro
|
||||
const messageInputRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
// Загружаем сообщения с контрагентом
|
||||
const {
|
||||
data: messagesData,
|
||||
loading,
|
||||
refetch,
|
||||
} = useQuery(GET_MESSAGES, {
|
||||
const { data: messagesData, loading, refetch } = useQuery(GET_MESSAGES, {
|
||||
variables: { counterpartyId: counterparty.id },
|
||||
pollInterval: 3000,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
errorPolicy: 'all',
|
||||
})
|
||||
@ -120,6 +116,27 @@ export function MessengerChat({ counterparty, onMessagesRead }: MessengerChatPro
|
||||
|
||||
const messages = useMemo(() => messagesData?.messages || [], [messagesData?.messages])
|
||||
|
||||
// Реалтайм: обновляем чат при поступлении новых сообщений
|
||||
useRealtime({
|
||||
onEvent: (evt) => {
|
||||
if (evt.type !== 'message:new') return
|
||||
const { senderOrgId, receiverOrgId } = (evt.payload || {}) as {
|
||||
senderOrgId?: string
|
||||
receiverOrgId?: string
|
||||
}
|
||||
if (!user?.organization?.id) return
|
||||
// Refetch if event is for this conversation
|
||||
if (
|
||||
senderOrgId === counterparty.id ||
|
||||
receiverOrgId === counterparty.id ||
|
||||
senderOrgId === user.organization.id ||
|
||||
receiverOrgId === user.organization.id
|
||||
) {
|
||||
refetch()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Отмечаем сообщения как прочитанные только если есть непрочитанные
|
||||
useEffect(() => {
|
||||
if (messages.length > 0 && user?.organization?.id && counterparty.id) {
|
||||
|
@ -10,6 +10,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { GET_CONVERSATIONS, GET_MY_COUNTERPARTIES } from '@/graphql/queries'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
import { useRealtime } from '@/hooks/useRealtime'
|
||||
|
||||
import { MessengerChat } from './messenger-chat'
|
||||
import { MessengerConversations } from './messenger-conversations'
|
||||
@ -48,14 +49,19 @@ export function MessengerDashboard() {
|
||||
const [selectedCounterparty, setSelectedCounterparty] = useState<string | null>(null)
|
||||
|
||||
// Загружаем список чатов (conversations) для отображения непрочитанных сообщений
|
||||
const {
|
||||
data: conversationsData,
|
||||
loading: conversationsLoading,
|
||||
refetch: refetchConversations,
|
||||
} = useQuery(GET_CONVERSATIONS, {
|
||||
pollInterval: 30000, // Обновляем каждые 30 секунд - реже, но достаточно
|
||||
fetchPolicy: 'cache-first', // Приоритет кэшу для стабильности
|
||||
notifyOnNetworkStatusChange: false, // Не показываем загрузку при фоновых обновлениях
|
||||
const { data: conversationsData, loading: conversationsLoading, refetch: refetchConversations } = useQuery(
|
||||
GET_CONVERSATIONS,
|
||||
{
|
||||
fetchPolicy: 'cache-first', // Приоритет кэшу для стабильности
|
||||
notifyOnNetworkStatusChange: false, // Не показываем загрузку при фоновых обновлениях
|
||||
},
|
||||
)
|
||||
|
||||
// Realtime: обновление списка бесед
|
||||
useRealtime({
|
||||
onEvent: (evt) => {
|
||||
if (evt.type === 'message:new') refetchConversations()
|
||||
},
|
||||
})
|
||||
|
||||
// Также загружаем полный список контрагентов на случай, если с кем-то еще не общались
|
||||
|
@ -7,6 +7,7 @@ import { Card } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { GET_INCOMING_REQUESTS } from '@/graphql/queries'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
import { useRealtime } from '@/hooks/useRealtime'
|
||||
|
||||
import { MarketCounterparties } from '../market/market-counterparties'
|
||||
import { MarketFulfillment } from '../market/market-fulfillment'
|
||||
@ -20,12 +21,19 @@ export function PartnersDashboard() {
|
||||
const { getSidebarMargin } = useSidebar()
|
||||
|
||||
// Загружаем входящие заявки для подсветки
|
||||
const { data: incomingRequestsData } = useQuery(GET_INCOMING_REQUESTS, {
|
||||
pollInterval: 30000, // Обновляем каждые 30 секунд
|
||||
const { data: incomingRequestsData, refetch: refetchIncoming } = useQuery(GET_INCOMING_REQUESTS, {
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore',
|
||||
})
|
||||
|
||||
useRealtime({
|
||||
onEvent: (evt) => {
|
||||
if (evt.type === 'counterparty:request:new' || evt.type === 'counterparty:request:updated') {
|
||||
refetchIncoming()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const incomingRequests = incomingRequestsData?.incomingRequests || []
|
||||
const hasIncomingRequests = incomingRequests.length > 0
|
||||
|
||||
|
@ -35,6 +35,7 @@ import {
|
||||
} from '@/graphql/mutations'
|
||||
import { GET_SUPPLY_ORDERS } from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useRealtime } from '@/hooks/useRealtime'
|
||||
|
||||
// Типы для данных заказов
|
||||
interface SupplyOrderItem {
|
||||
@ -167,7 +168,15 @@ export function RealSupplyOrdersTab() {
|
||||
const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
notifyOnNetworkStatusChange: true,
|
||||
pollInterval: 30000, // 🔔 Опрашиваем каждые 30 секунд для получения новых заказов
|
||||
})
|
||||
|
||||
// Realtime: обновление списка заказов
|
||||
useRealtime({
|
||||
onEvent: (evt) => {
|
||||
if (evt.type === 'supply-order:new' || evt.type === 'supply-order:updated') {
|
||||
refetch()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Мутация для обновления статуса заказа
|
||||
|
@ -11,6 +11,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { GET_PENDING_SUPPLIES_COUNT } from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
import { useRealtime } from '@/hooks/useRealtime'
|
||||
|
||||
import { AllSuppliesTab } from './fulfillment-supplies/all-supplies-tab'
|
||||
import { RealSupplyOrdersTab } from './fulfillment-supplies/real-supply-orders-tab'
|
||||
@ -39,12 +40,19 @@ export function SuppliesDashboard() {
|
||||
const [statisticsData, setStatisticsData] = useState<any>(null)
|
||||
|
||||
// Загружаем счетчик поставок, требующих одобрения
|
||||
const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
pollInterval: 30000, // Обновляем каждые 30 секунд
|
||||
const { data: pendingData, refetch: refetchPending } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore',
|
||||
})
|
||||
|
||||
useRealtime({
|
||||
onEvent: (evt) => {
|
||||
if (evt.type === 'supply-order:new' || evt.type === 'supply-order:updated') {
|
||||
refetchPending()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const pendingCount = pendingData?.pendingSuppliesCount
|
||||
// ✅ ПРАВИЛЬНО: Настраиваем уведомления по типам организаций
|
||||
const hasPendingItems = (() => {
|
||||
|
Reference in New Issue
Block a user