fix some bugs from chart
This commit is contained in:
@@ -21,6 +21,18 @@ export const getOnlineCustomers = async () =>
|
|||||||
rows: Customer[]
|
rows: Customer[]
|
||||||
}>('/admin/customer/online')
|
}>('/admin/customer/online')
|
||||||
|
|
||||||
|
/** حضور مشتریان دارای روم چت — برای نقطهٔ آنلاین لیست/هدر پشتیبانی */
|
||||||
|
export const getCustomerChatPresence = async () =>
|
||||||
|
await useAxios.get<{
|
||||||
|
online_within_seconds: number
|
||||||
|
rows: Array<{
|
||||||
|
id: number
|
||||||
|
chat_room_id?: string | null
|
||||||
|
chat_user_id?: string | null
|
||||||
|
last_seen_at?: string | null
|
||||||
|
}>
|
||||||
|
}>('/admin/customer/chat-presence')
|
||||||
|
|
||||||
export const getCustomer = async (id: number) =>
|
export const getCustomer = async (id: number) =>
|
||||||
await useAxios.get<Customer>(`/admin/customer/${id}`)
|
await useAxios.get<Customer>(`/admin/customer/${id}`)
|
||||||
|
|
||||||
|
|||||||
@@ -283,7 +283,7 @@
|
|||||||
// import BasicInfoModal from '@/components/BasicInfoModal'
|
// import BasicInfoModal from '@/components/BasicInfoModal'
|
||||||
import { inputLenRules, inputRules } from '@/utils/inputRules'
|
import { inputLenRules, inputRules } from '@/utils/inputRules'
|
||||||
import { useStaticApiListsStore } from '@/store/staticApisStore'
|
import { useStaticApiListsStore } from '@/store/staticApisStore'
|
||||||
import { ref, watch, computed, onMounted } from 'vue'
|
import { ref, watch, computed, onMounted, nextTick } from 'vue'
|
||||||
import tbl from './components/table.vue'
|
import tbl from './components/table.vue'
|
||||||
import type { FormInstance } from 'ant-design-vue'
|
import type { FormInstance } from 'ant-design-vue'
|
||||||
import BasicInfoModal from '@/components/BasicInfoModal/index.vue'
|
import BasicInfoModal from '@/components/BasicInfoModal/index.vue'
|
||||||
@@ -498,21 +498,76 @@ function infoOrder(val: Order) {
|
|||||||
userInfoOrderVisible.value = true
|
userInfoOrderVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** جلوگیری از race وقتی چند بار پشتسرهم مدال باز میشود */
|
||||||
|
let infoLoadSeq = 0
|
||||||
|
|
||||||
|
async function refetchCustomerProfile(customerId?: number | null) {
|
||||||
|
const id = customerId ?? userInfo.value?.id
|
||||||
|
if (id == null) return
|
||||||
|
const seq = ++infoLoadSeq
|
||||||
|
try {
|
||||||
|
const fresh = await getCustomer(id)
|
||||||
|
if (seq !== infoLoadSeq || !fresh) return
|
||||||
|
userInfo.value = cloneDeep(fresh)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
if (seq === infoLoadSeq) {
|
||||||
|
message.warning('بهروزرسانی مشخصات مشتری ناموفق بود؛ دادههای جدول نمایش داده شد')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** رفرش دادهٔ همان تب فعال داخل مدال مشخصات */
|
||||||
|
async function refreshInfoTab(key: string) {
|
||||||
|
if (!userInfoVisible.value || !userInfo.value?.id) return
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'اطلاعات جامع':
|
||||||
|
void loadInfoChargeTargets()
|
||||||
|
await Promise.allSettled([refetchCustomerProfile(), refreshWalletPanels()])
|
||||||
|
break
|
||||||
|
case 'لیست سفارشات':
|
||||||
|
await nextTick()
|
||||||
|
refreshOrderTable()
|
||||||
|
break
|
||||||
|
case 'لیست تراکنش ها':
|
||||||
|
await nextTick()
|
||||||
|
refreshTransactionTable()
|
||||||
|
break
|
||||||
|
case 'پیامک ها':
|
||||||
|
await nextTick()
|
||||||
|
refreshSMSTable()
|
||||||
|
break
|
||||||
|
case 'معرفی کرده ها':
|
||||||
|
await nextTick()
|
||||||
|
refreshReferralTable()
|
||||||
|
break
|
||||||
|
case 'قلک ها':
|
||||||
|
await nextTick()
|
||||||
|
refreshFundTable()
|
||||||
|
break
|
||||||
|
case 'لاگ پرگار':
|
||||||
|
await nextTick()
|
||||||
|
refreshPargarLogsTable()
|
||||||
|
break
|
||||||
|
case 'سفارش شرطی':
|
||||||
|
await refetchCustomerProfile()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function info(val: Customer) {
|
async function info(val: Customer) {
|
||||||
activeKey.value = 'اطلاعات جامع'
|
// فوری از ردیف جدول نشان بده؛ بعد تکی از API تازه میگیریم
|
||||||
userInfo.value = cloneDeep(val)
|
userInfo.value = cloneDeep(val)
|
||||||
userInfoVisible.value = true
|
userInfoVisible.value = true
|
||||||
void loadInfoChargeTargets()
|
// اگر تب از قبل «اطلاعات جامع» بود watch شلیک نمیکند — دستی رفرش کن
|
||||||
await nextTick(() => {
|
const alreadyOnOverview = activeKey.value === 'اطلاعات جامع'
|
||||||
loading.value = true
|
activeKey.value = 'اطلاعات جامع'
|
||||||
refreshFundTable()
|
if (alreadyOnOverview) {
|
||||||
refreshPargarLogsTable()
|
await refreshInfoTab('اطلاعات جامع')
|
||||||
refreshReferralTable()
|
}
|
||||||
refreshSMSTable()
|
|
||||||
refreshTransactionTable()
|
|
||||||
refreshOrderTable()
|
|
||||||
loading.value = false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//edit
|
//edit
|
||||||
@@ -598,6 +653,12 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** با هر عوض شدن تب داخل مدال، دادهٔ همان تب دوباره از API گرفته شود */
|
||||||
|
watch(activeKey, (key) => {
|
||||||
|
if (!userInfoVisible.value) return
|
||||||
|
void refreshInfoTab(key)
|
||||||
|
})
|
||||||
|
|
||||||
//add project modal
|
//add project modal
|
||||||
const addUserVisible = ref<boolean>(false)
|
const addUserVisible = ref<boolean>(false)
|
||||||
const userInfoVisible = ref<boolean>(false)
|
const userInfoVisible = ref<boolean>(false)
|
||||||
|
|||||||
@@ -11,7 +11,11 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="admin-support-header__identity">
|
<div class="admin-support-header__identity">
|
||||||
<div class="admin-support-header__avatar" aria-hidden="true">
|
<div
|
||||||
|
class="admin-support-header__avatar"
|
||||||
|
:class="{ 'admin-support-header__avatar--online': online }"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-if="avatar"
|
v-if="avatar"
|
||||||
:src="avatar"
|
:src="avatar"
|
||||||
@@ -21,13 +25,18 @@
|
|||||||
<span v-else class="admin-support-header__avatar-text">
|
<span v-else class="admin-support-header__avatar-text">
|
||||||
{{ titleInitial }}
|
{{ titleInitial }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="online" class="admin-support-header__online" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-support-header__text min-w-0 flex-1">
|
<div class="admin-support-header__text min-w-0 flex-1">
|
||||||
<h1 class="admin-support-header__title">
|
<h1 class="admin-support-header__title">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h1>
|
</h1>
|
||||||
<p v-if="subtitle" class="admin-support-header__subtitle">
|
<p
|
||||||
|
v-if="subtitle"
|
||||||
|
class="admin-support-header__subtitle"
|
||||||
|
:class="{ 'admin-support-header__subtitle--online': online }"
|
||||||
|
>
|
||||||
{{ subtitle }}
|
{{ subtitle }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,6 +52,7 @@ const props = defineProps<{
|
|||||||
title: string
|
title: string
|
||||||
subtitle: string
|
subtitle: string
|
||||||
avatar: string
|
avatar: string
|
||||||
|
online?: boolean
|
||||||
mobileListOpen: boolean
|
mobileListOpen: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -97,13 +107,14 @@ const titleInitial = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-support-header__avatar {
|
.admin-support-header__avatar {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 0 40px;
|
flex: 0 0 40px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid var(--admin-border-strong, #e4e4e7);
|
border: 1px solid var(--admin-border-strong, #e4e4e7);
|
||||||
background: color-mix(in srgb, var(--admin-gold-light, #2563eb) 14%, var(--admin-bg-muted, #eff6ff));
|
background: color-mix(in srgb, var(--admin-gold-light, #2563eb) 14%, var(--admin-bg-muted, #eff6ff));
|
||||||
@@ -116,12 +127,26 @@ const titleInitial = computed(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-support-header__avatar-text {
|
.admin-support-header__avatar-text {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-support-header__online {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #22c55e;
|
||||||
|
border: 2px solid var(--admin-bg-elevated, #fff);
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.45);
|
||||||
|
animation: admin-support-online-pulse 1.6s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-support-header__title {
|
.admin-support-header__title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -144,6 +169,23 @@ const titleInitial = computed(() => {
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-support-header__subtitle--online {
|
||||||
|
color: #16a34a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes admin-support-online-pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.45);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 6px rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.admin-support-header {
|
.admin-support-header {
|
||||||
padding: 14px 20px;
|
padding: 14px 20px;
|
||||||
@@ -169,4 +211,12 @@ html.is-dark .admin-support-header__avatar {
|
|||||||
border-color: var(--admin-border-strong);
|
border-color: var(--admin-border-strong);
|
||||||
color: var(--admin-gold-light);
|
color: var(--admin-gold-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.is-dark .admin-support-header__online {
|
||||||
|
border-color: var(--admin-bg-elevated, #18181b);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.is-dark .admin-support-header__subtitle--online {
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -466,7 +466,7 @@ const getAvatarText = (name: string) => {
|
|||||||
height: 46px;
|
height: 46px;
|
||||||
flex: 0 0 46px;
|
flex: 0 0 46px;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
background: color-mix(in srgb, var(--admin-gold-light, #2563eb) 16%, var(--admin-bg-elevated, #eef2ff));
|
background: color-mix(in srgb, var(--admin-gold-light, #2563eb) 16%, var(--admin-bg-elevated, #eef2ff));
|
||||||
color: var(--admin-gold-light, #2563eb);
|
color: var(--admin-gold-light, #2563eb);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -479,6 +479,7 @@ const getAvatarText = (name: string) => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
border-radius: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-room-card__avatar-text {
|
.admin-room-card__avatar-text {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
:title="activeConversation.userName"
|
:title="activeConversation.userName"
|
||||||
:subtitle="activeConversation.online ? 'آنلاین' : activeConversation.lastSeenText"
|
:subtitle="activeConversation.online ? 'آنلاین' : activeConversation.lastSeenText"
|
||||||
:avatar="activeConversation.avatar"
|
:avatar="activeConversation.avatar"
|
||||||
|
:online="activeConversation.online"
|
||||||
:mobile-list-open="mobileListOpen"
|
:mobile-list-open="mobileListOpen"
|
||||||
@back-to-list="handleBackToList"
|
@back-to-list="handleBackToList"
|
||||||
@toggle-mobile-list="mobileListOpen = !mobileListOpen"
|
@toggle-mobile-list="mobileListOpen = !mobileListOpen"
|
||||||
@@ -101,6 +102,53 @@ import {
|
|||||||
setAdminActiveRoomAtBottom
|
setAdminActiveRoomAtBottom
|
||||||
} from '@/utils/adminChat'
|
} from '@/utils/adminChat'
|
||||||
import { useUserStore } from '@/store/userStore'
|
import { useUserStore } from '@/store/userStore'
|
||||||
|
import { getCustomerChatPresence } from '@/api/customer'
|
||||||
|
import {
|
||||||
|
formatCustomerPresenceLabel,
|
||||||
|
isCustomerOnline
|
||||||
|
} from '@/utils/customerPresence'
|
||||||
|
|
||||||
|
const PRESENCE_POLL_MS = 30_000
|
||||||
|
|
||||||
|
/** last_seen_at keyed by room_id و chat_user_id */
|
||||||
|
const presenceByRoomId = ref<Record<string, string | null>>({})
|
||||||
|
const presenceByUserId = ref<Record<string, string | null>>({})
|
||||||
|
let presencePollTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
async function loadChatPresence() {
|
||||||
|
try {
|
||||||
|
const res = await getCustomerChatPresence()
|
||||||
|
const byRoom: Record<string, string | null> = {}
|
||||||
|
const byUser: Record<string, string | null> = {}
|
||||||
|
|
||||||
|
for (const row of res?.rows ?? []) {
|
||||||
|
const seen = row.last_seen_at ?? null
|
||||||
|
if (row.chat_room_id) byRoom[String(row.chat_room_id)] = seen
|
||||||
|
if (row.chat_user_id) byUser[String(row.chat_user_id)] = seen
|
||||||
|
byUser[`user_${row.id}`] = seen
|
||||||
|
}
|
||||||
|
|
||||||
|
presenceByRoomId.value = byRoom
|
||||||
|
presenceByUserId.value = byUser
|
||||||
|
} catch (e) {
|
||||||
|
console.error('chat presence load failed:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPresencePolling() {
|
||||||
|
void loadChatPresence()
|
||||||
|
if (presencePollTimer) clearInterval(presencePollTimer)
|
||||||
|
presencePollTimer = setInterval(() => {
|
||||||
|
void loadChatPresence()
|
||||||
|
}, PRESENCE_POLL_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPresencePolling() {
|
||||||
|
if (presencePollTimer) {
|
||||||
|
clearInterval(presencePollTimer)
|
||||||
|
presencePollTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
@@ -412,6 +460,32 @@ const getConversationName = (room: any) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveRoomLastSeen(room: any): string | null | undefined {
|
||||||
|
const roomId = getRoomId(room)
|
||||||
|
if (roomId && Object.prototype.hasOwnProperty.call(presenceByRoomId.value, roomId)) {
|
||||||
|
return presenceByRoomId.value[roomId]
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = [
|
||||||
|
room?.user_id,
|
||||||
|
room?.user?.id,
|
||||||
|
room?.chat_user_id,
|
||||||
|
room?.customer?.chat_user_id,
|
||||||
|
room?.peer_id,
|
||||||
|
room?.peer?.id
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const raw of candidates) {
|
||||||
|
if (raw == null || raw === '' || raw === 'admin') continue
|
||||||
|
const key = String(raw)
|
||||||
|
if (Object.prototype.hasOwnProperty.call(presenceByUserId.value, key)) {
|
||||||
|
return presenceByUserId.value[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const mapRawAttachment = (item: any): SupportAttachment | null => {
|
const mapRawAttachment = (item: any): SupportAttachment | null => {
|
||||||
if (!item) return null
|
if (!item) return null
|
||||||
|
|
||||||
@@ -574,9 +648,21 @@ const conversations = computed<SupportConversation[]>(() => {
|
|||||||
id: getRoomId(room),
|
id: getRoomId(room),
|
||||||
userName: getConversationName(room),
|
userName: getConversationName(room),
|
||||||
avatar: room?.avatar || room?.user?.avatar || '',
|
avatar: room?.avatar || room?.user?.avatar || '',
|
||||||
online: Boolean(room?.online ?? room?.user?.online ?? false),
|
online: (() => {
|
||||||
|
const lastSeen = resolveRoomLastSeen(room)
|
||||||
|
if (lastSeen === undefined) {
|
||||||
|
return Boolean(room?.online ?? room?.user?.online ?? false)
|
||||||
|
}
|
||||||
|
return isCustomerOnline(lastSeen)
|
||||||
|
})(),
|
||||||
unreadCount: Number(room?.unread_count ?? room?.unreadCount ?? 0),
|
unreadCount: Number(room?.unread_count ?? room?.unreadCount ?? 0),
|
||||||
lastSeenText: room?.last_seen_text || room?.lastSeenText || 'نامشخص',
|
lastSeenText: (() => {
|
||||||
|
const lastSeen = resolveRoomLastSeen(room)
|
||||||
|
if (lastSeen === undefined) {
|
||||||
|
return room?.last_seen_text || room?.lastSeenText || 'آفلاین'
|
||||||
|
}
|
||||||
|
return formatCustomerPresenceLabel(lastSeen)
|
||||||
|
})(),
|
||||||
lastMessagePreview,
|
lastMessagePreview,
|
||||||
updatedAtText: formatTime(
|
updatedAtText: formatTime(
|
||||||
room?.last_message?.created_at || room?.updated_at || room?.created_at || new Date()
|
room?.last_message?.created_at || room?.updated_at || room?.created_at || new Date()
|
||||||
@@ -788,6 +874,7 @@ const sendAdminReply = async (payload: ComposerSendPayload) => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setAdminSupportPageOpen(true)
|
setAdminSupportPageOpen(true)
|
||||||
|
startPresencePolling()
|
||||||
if (isMobileViewport()) {
|
if (isMobileViewport()) {
|
||||||
mobileListOpen.value = !activeConversationId.value
|
mobileListOpen.value = !activeConversationId.value
|
||||||
}
|
}
|
||||||
@@ -795,10 +882,12 @@ onMounted(() => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
setAdminSupportPageOpen(false)
|
setAdminSupportPageOpen(false)
|
||||||
|
stopPresencePolling()
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
setAdminSupportPageOpen(true)
|
setAdminSupportPageOpen(true)
|
||||||
|
startPresencePolling()
|
||||||
if (isMobileViewport() && !activeConversationId.value) {
|
if (isMobileViewport() && !activeConversationId.value) {
|
||||||
mobileListOpen.value = true
|
mobileListOpen.value = true
|
||||||
}
|
}
|
||||||
@@ -806,6 +895,7 @@ onActivated(() => {
|
|||||||
|
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
setAdminSupportPageOpen(false)
|
setAdminSupportPageOpen(false)
|
||||||
|
stopPresencePolling()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
|
||||||
/** همتراز با CustomerPresenceService::ONLINE_THRESHOLD_SECONDS روی سرور */
|
/** همتراز با CustomerPresenceService::ONLINE_THRESHOLD_SECONDS روی سرور */
|
||||||
export const CUSTOMER_ONLINE_THRESHOLD_MS = 2 * 60 * 1000
|
export const CUSTOMER_ONLINE_THRESHOLD_MS = 2 * 60 * 1000
|
||||||
|
|
||||||
@@ -16,3 +18,13 @@ export function getCustomerPresenceKind(
|
|||||||
export function isCustomerOnline(lastSeenAt: string | Date | null | undefined): boolean {
|
export function isCustomerOnline(lastSeenAt: string | Date | null | undefined): boolean {
|
||||||
return getCustomerPresenceKind(lastSeenAt) === 'online'
|
return getCustomerPresenceKind(lastSeenAt) === 'online'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** متن وضعیت برای چت (بهجای «نامشخص») */
|
||||||
|
export function formatCustomerPresenceLabel(
|
||||||
|
lastSeenAt: string | Date | null | undefined
|
||||||
|
): string {
|
||||||
|
const kind = getCustomerPresenceKind(lastSeenAt)
|
||||||
|
if (kind === 'online') return 'آنلاین'
|
||||||
|
if (kind === 'never') return 'آفلاین'
|
||||||
|
return lastSeenAt ? `آخرین بازدید ${toJalali(lastSeenAt as string)}` : 'آفلاین'
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user