From e8d5dc9f66d50c40fdb1420ed0f817a76d002533 Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 4 Aug 2026 18:59:52 +0330 Subject: [PATCH] added trigger for chat sms --- src/api/chatFocus.ts | 17 ++++ .../menuPages/configuration/settingTypes.ts | 6 +- src/utils/adminChat.ts | 84 ++++++++++++++++++- 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/api/chatFocus.ts diff --git a/src/api/chatFocus.ts b/src/api/chatFocus.ts new file mode 100644 index 0000000..7c80ebf --- /dev/null +++ b/src/api/chatFocus.ts @@ -0,0 +1,17 @@ +import useAxios from '@core/utils/useAxios' + +/** اعلام تمرکز ادمین روی اتاق پشتیبانی (برای جلوگیری از SMS وقتی ادمین همان چت را می‌بیند) */ +export const postAdminChatFocus = async (payload: { + room_id?: string | null + previous_room_id?: string | null +}) => + await useAxios.post('/admin/chat/focus', { + room_id: payload.room_id || '', + previous_room_id: payload.previous_room_id || '' + }) + +export const postAdminChatUnfocus = async (room_id: string) => + await useAxios.post('/admin/chat/unfocus', { room_id }) + +export const postAdminChatEnsureWebhook = async () => + await useAxios.post('/admin/chat/ensure-webhook') diff --git a/src/pages/menuPages/configuration/settingTypes.ts b/src/pages/menuPages/configuration/settingTypes.ts index 3e47780..fad4411 100644 --- a/src/pages/menuPages/configuration/settingTypes.ts +++ b/src/pages/menuPages/configuration/settingTypes.ts @@ -317,6 +317,7 @@ const KEY_TYPE_FALLBACK: Record = { sms_notif_fund_withdraw_customer: 'switch', sms_notif_price_trigger: 'switch', sms_notif_wallet_alert: 'switch', + sms_notif_support_admin: 'switch', group_sms: 'switch', montzar_closed: 'switch', montzar_open_buy: 'switch', @@ -798,6 +799,8 @@ const KEY_HELP_FALLBACK: Record = { 'روشن: وقتی هشدار قیمت مشتری (اقدام=پیامک) فعال شود، پیامک به مشتری می‌رود. خاموش: هشدار قیمت پیامک نمی‌فرستد.', sms_notif_wallet_alert: 'روشن: وقتی اعتبار پنل پیامک صفر یا منفی شود، هشدار به سوپرادمین و ادمین‌ها می‌رود. خاموش: این هشدار نمی‌رود.', + sms_notif_support_admin: + 'روشن: وقتی مشتری اولین پیام خوانده‌نشده را در چت پشتیبانی می‌فرستد و ادمین همان گفتگو را باز و visible ندارد، پیامک به ادمین‌های دارای «ارسال پیامک» می‌رود. خاموش: اعلان پیامک چت نمی‌رود.', group_sms: 'روشن: امکان ارسال پیامک گروهی از پنل ادمین فعال است. خاموش: ارسال پیامک گروهی غیرفعال است.', profit_gold_api_enabled: @@ -977,6 +980,7 @@ export const KEY_LABEL_FALLBACK: Record = { pargar_item_id_rial: 'کد کالای ریال پرگار', pargar_item_id_gold: 'کد کالای طلا پرگار', sms_notif_wallet_alert: 'پیامک هشدار اعتبار پنل پیامک', + sms_notif_support_admin: 'پیامک پیام پشتیبانی به ادمین', auto_active_customer: 'فعال‌سازی خودکار پس از ثبت‌نام', balance_report_exclude_customer_id: 'حذف مشتری از گزارش تراز', chart_use_local_data: 'نمودار از داده داخلی', @@ -1097,7 +1101,7 @@ export const SETTING_EDITABLE_KEYS = new Set([ 'sms_notif_gateway_charge_admin', 'sms_notif_register_customer', 'sms_notif_register_admin', 'sms_notif_jibit_admin', 'sms_notif_bank_card_admin', 'sms_notif_bank_card_customer', 'sms_notif_fund_withdraw_admin', 'sms_notif_fund_withdraw_customer', - 'sms_notif_price_trigger', 'sms_notif_wallet_alert', + 'sms_notif_price_trigger', 'sms_notif_wallet_alert', 'sms_notif_support_admin', 'deposit_sms_support_number', 'deposit_support_message_text', 'deposit_account_holder', 'deposit_card_number', 'deposit_sheba', 'endDay', diff --git a/src/utils/adminChat.ts b/src/utils/adminChat.ts index 324acfe..6a1d506 100644 --- a/src/utils/adminChat.ts +++ b/src/utils/adminChat.ts @@ -2,6 +2,7 @@ import { computed, ref, watch } from 'vue' import { useWebSocket } from '@vueuse/core' import { useUserStore } from '@/store/userStore' import { createUuid } from '@/utils/index' +import { postAdminChatFocus, postAdminChatUnfocus, postAdminChatEnsureWebhook } from '@/api/chatFocus' type RawChatRoom = Record type RawChatMessage = Record @@ -29,6 +30,7 @@ const MAX_RESEND_ATTEMPTS = 1 const CONNECTING_TIMEOUT_MS = 12000 const RECONNECT_WATCH_INTERVAL_MS = 5000 const READ_SIGNAL_DEBOUNCE_MS = 1200 +const FOCUS_HEARTBEAT_MS = 20000 type AdminOutgoingMessageType = 'text' | 'image' | 'voice' @@ -70,6 +72,8 @@ const lastReadSignalAt = ref(0) const lastReadSignalRoomId = ref('') let readSignalTimer: number | null = null let pendingAdminReadSignalRoomId = '' +let focusHeartbeatTimer: number | null = null +let lastReportedFocusRoomId = '' let bootstrapPromise: Promise | null = null @@ -132,7 +136,12 @@ export const setAdminSupportPageOpen = (value: boolean) => { isAdminActiveRoomAtBottom.value = false clearReadSignalTimer() pendingAdminReadSignalRoomId = '' + const previousRoom = activeRoomId.value || lastReportedFocusRoomId activeRoomId.value = '' + stopFocusHeartbeat() + if (previousRoom) { + void syncAdminChatFocus(null, previousRoom) + } return } @@ -146,6 +155,8 @@ export const setAdminSupportPageOpen = (value: boolean) => { scheduleAdminReadSignal(80) } + refreshFocusHeartbeat() + if (value && !manuallyClosed && status.value !== 'OPEN') { safeReconnect('admin support page opened').catch((error) => { console.error('admin reconnect on page open failed:', error) @@ -164,6 +175,63 @@ const isViewingRoomNow = (roomId: string) => { ) } +const syncAdminChatFocus = async (roomId: string | null, previousRoomId?: string) => { + try { + if (!roomId) { + if (previousRoomId || lastReportedFocusRoomId) { + await postAdminChatUnfocus(previousRoomId || lastReportedFocusRoomId) + } + lastReportedFocusRoomId = '' + return + } + + await postAdminChatFocus({ + room_id: roomId, + previous_room_id: previousRoomId && previousRoomId !== roomId ? previousRoomId : undefined + }) + lastReportedFocusRoomId = roomId + } catch (error) { + console.error('admin chat focus sync failed:', error) + } +} + +const stopFocusHeartbeat = () => { + if (focusHeartbeatTimer !== null) { + window.clearInterval(focusHeartbeatTimer) + focusHeartbeatTimer = null + } +} + +const refreshFocusHeartbeat = () => { + stopFocusHeartbeat() + + const roomId = activeRoomId.value + if (!isViewingRoomNow(roomId)) { + if (lastReportedFocusRoomId) { + void syncAdminChatFocus(null, lastReportedFocusRoomId) + } + return + } + + void syncAdminChatFocus( + roomId, + lastReportedFocusRoomId && lastReportedFocusRoomId !== roomId + ? lastReportedFocusRoomId + : undefined + ) + + focusHeartbeatTimer = window.setInterval(() => { + if (!isViewingRoomNow(activeRoomId.value)) { + stopFocusHeartbeat() + if (lastReportedFocusRoomId) { + void syncAdminChatFocus(null, lastReportedFocusRoomId) + } + return + } + void syncAdminChatFocus(activeRoomId.value) + }, FOCUS_HEARTBEAT_MS) +} + const canTreatAdminRoomAsRead = (roomId = activeRoomId.value) => { return Boolean( roomId && @@ -1032,7 +1100,13 @@ const bindNetworkListeners = () => { }) document.addEventListener('visibilitychange', () => { - if (document.visibilityState !== 'visible') return + if (document.visibilityState !== 'visible') { + stopFocusHeartbeat() + if (lastReportedFocusRoomId) { + void syncAdminChatFocus(null, lastReportedFocusRoomId) + } + return + } if (isAdminSupportPageOpen.value && activeRoomId.value) { if (canTreatAdminRoomAsRead(activeRoomId.value)) { @@ -1041,6 +1115,8 @@ const bindNetworkListeners = () => { } } + refreshFocusHeartbeat() + if (!shouldKeepAdminSocketAlive()) return if (status.value !== 'OPEN') { @@ -1435,6 +1511,8 @@ export const setAdminActiveRoom = async ( pendingAdminReadSignalRoomId = '' } + refreshFocusHeartbeat() + if (isViewingRoomNow(roomId)) { clearRoomUnread(roomId) } @@ -1609,6 +1687,10 @@ export const bootstrapAdminChat = async ( bootstrapPromise = (async () => { try { + void postAdminChatEnsureWebhook().catch((error) => { + console.error('admin chat ensure webhook failed:', error) + }) + await fetchAdminRooms() const shouldConnect = connectEvenIfNoRooms || rooms.value.length > 0