added online and trigger
This commit is contained in:
@@ -14,6 +14,16 @@ export const updateCustomer = async (id: number, obj: Customer) =>
|
|||||||
export const getCustomerTable = async (params: TableInput) =>
|
export const getCustomerTable = async (params: TableInput) =>
|
||||||
await useAxios.post<Array<Customer>>('/admin/customer/table', params)
|
await useAxios.post<Array<Customer>>('/admin/customer/table', params)
|
||||||
|
|
||||||
|
export const getOnlineCustomers = async () =>
|
||||||
|
await useAxios.get<{
|
||||||
|
count: number
|
||||||
|
online_within_seconds: number
|
||||||
|
rows: Customer[]
|
||||||
|
}>('/admin/customer/online')
|
||||||
|
|
||||||
|
export const getCustomer = async (id: number) =>
|
||||||
|
await useAxios.get<Customer>(`/admin/customer/${id}`)
|
||||||
|
|
||||||
export const getCustomerAssetsSummary = async (customerId: number) =>
|
export const getCustomerAssetsSummary = async (customerId: number) =>
|
||||||
await useAxios.get<WalletAssetsSummary>(`/admin/customer/${customerId}/assets`)
|
await useAxios.get<WalletAssetsSummary>(`/admin/customer/${customerId}/assets`)
|
||||||
|
|
||||||
|
|||||||
+18
-2
@@ -10,9 +10,25 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-tabs-rtl .ant-tabs-nav .ant-tabs-tab{
|
/*
|
||||||
margin: 10px !important;
|
Do NOT set equal margin on .ant-tabs-tab in RTL — it desyncs the ink-bar
|
||||||
|
from the active tab (Ant measures tab boxes; asymmetric/all-side margins skew it).
|
||||||
|
Use gap on the nav-list instead for consistent spacing everywhere.
|
||||||
|
*/
|
||||||
|
.ant-tabs > .ant-tabs-nav .ant-tabs-nav-list,
|
||||||
|
.ant-tabs > div > .ant-tabs-nav .ant-tabs-nav-list {
|
||||||
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-tabs-rtl .ant-tabs-nav .ant-tabs-tab,
|
||||||
|
.ant-tabs .ant-tabs-nav .ant-tabs-tab {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs .ant-tabs-nav .ant-tabs-tab {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
/*::-webkit-scrollbar-track {
|
/*::-webkit-scrollbar-track {
|
||||||
background-color: #f2f6ff !important;
|
background-color: #f2f6ff !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
:columns="_columns"
|
:columns="_columns"
|
||||||
:data-source="data"
|
:data-source="data"
|
||||||
v-bind="_props"
|
v-bind="_props"
|
||||||
|
:scroll="tableScroll"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
@resizeColumn="handleResizeColumn"
|
@resizeColumn="handleResizeColumn"
|
||||||
>
|
>
|
||||||
@@ -289,24 +290,9 @@ import {
|
|||||||
availableFilterType,
|
availableFilterType,
|
||||||
MapFilterOpToFaString
|
MapFilterOpToFaString
|
||||||
} from '@core/components/BaseTable/hooks/filterOperator'
|
} from '@core/components/BaseTable/hooks/filterOperator'
|
||||||
//import { mapColumnTypeFilterToFilterType } from '@core/type/baseTable'
|
|
||||||
//import { useElementSize } from '@vueuse/core'
|
/** Wide enough for dense action icon rows (e.g. customers); too-narrow fixed cells bleed into neighbors. */
|
||||||
//import { ContentLoader } from 'vue-content-loader'
|
const DEFAULT_ACTION_WIDTH = 280
|
||||||
//skeleton
|
|
||||||
//const skeletonWidth = ref(null)
|
|
||||||
/*const skeletonWidthSize = reactive(
|
|
||||||
useElementSize(skeletonWidth, { width: 0, height: 0 }, { box: 'border-box' })
|
|
||||||
)*/
|
|
||||||
//const skeletonHeight = 357
|
|
||||||
/*const skeletonHeight = ref(null)
|
|
||||||
const skeletonHeightSize = reactive(
|
|
||||||
useElementSize(skeletonHeight, { width: 0, height: 0 }, { box: 'border-box' })
|
|
||||||
)*/
|
|
||||||
/*
|
|
||||||
watch(skeletonHeightSize, (v) => {
|
|
||||||
console.log('ttt', v)
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bordered?: boolean
|
bordered?: boolean
|
||||||
@@ -328,7 +314,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
driver: 'laravel-sql',
|
driver: 'laravel-sql',
|
||||||
mode: 'pro',
|
mode: 'pro',
|
||||||
columns: () => [],
|
columns: () => [],
|
||||||
scroll: () => ({ x: true }), //y: '100%' //y: 600
|
scroll: () => ({ x: true }),
|
||||||
pageSize: () => [5, 10, 20, 50, 100],
|
pageSize: () => [5, 10, 20, 50, 100],
|
||||||
defaultPageLimit: 10,
|
defaultPageLimit: 10,
|
||||||
showPaginate: true,
|
showPaginate: true,
|
||||||
@@ -345,13 +331,30 @@ const filters = ref<Filter[]>([])
|
|||||||
const order = ref<'asc' | 'desc'>('asc')
|
const order = ref<'asc' | 'desc'>('asc')
|
||||||
const orderColumnKey = ref<string | null>(null)
|
const orderColumnKey = ref<string | null>(null)
|
||||||
const containerRef = ref()
|
const containerRef = ref()
|
||||||
//const route = useRoute()
|
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
const tableReady = ref(false)
|
const tableReady = ref(false)
|
||||||
let callApiPromise: Promise<void> | null = null
|
let callApiPromise: Promise<void> | null = null
|
||||||
const _props = computed(() => {
|
const _props = computed(() => {
|
||||||
return omit(props, ['columns', 'pagination', 'dataSource'])
|
return omit(props, ['columns', 'pagination', 'dataSource', 'scroll'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const tableScroll = computed(() => ({
|
||||||
|
x: props.scroll?.x ?? true,
|
||||||
|
...(props.scroll?.y != null ? { y: props.scroll.y } : {})
|
||||||
|
}))
|
||||||
|
|
||||||
|
function isActionColumn(column: BaseTableColumn) {
|
||||||
|
const rawIndex = column.dataIndex
|
||||||
|
const dataIndex = Array.isArray(rawIndex) ? rawIndex.join('.') : String(rawIndex ?? '')
|
||||||
|
const key = column.key != null ? String(column.key) : ''
|
||||||
|
return (
|
||||||
|
column.slot === 'action' ||
|
||||||
|
key === 'action' ||
|
||||||
|
key === 'row_action' ||
|
||||||
|
dataIndex === 'action' ||
|
||||||
|
dataIndex === 'row_action'
|
||||||
|
)
|
||||||
|
}
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'register', data: BaseTableEmit): void
|
(e: 'register', data: BaseTableEmit): void
|
||||||
}>()
|
}>()
|
||||||
@@ -469,15 +472,17 @@ const _columns = computed(() => {
|
|||||||
if (!Object.hasOwn(column, 'align')) {
|
if (!Object.hasOwn(column, 'align')) {
|
||||||
column.align = 'center'
|
column.align = 'center'
|
||||||
}
|
}
|
||||||
|
if (isActionColumn(column)) {
|
||||||
|
if (!Object.hasOwn(column, 'fixed') || column.fixed == null) {
|
||||||
|
column.fixed = 'right'
|
||||||
|
}
|
||||||
|
const w = Number(column.width)
|
||||||
|
if (!Object.hasOwn(column, 'width') || column.width == null || !Number.isFinite(w) || w < 200) {
|
||||||
|
column.width = DEFAULT_ACTION_WIDTH
|
||||||
|
}
|
||||||
|
}
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
column.dataIndex = column.dataIndex.split('.')
|
column.dataIndex = String(column.dataIndex ?? '').split('.')
|
||||||
//if(isArray(column.dataIndex))
|
|
||||||
/*if(!isArray(column.dataIndex)){
|
|
||||||
}*/
|
|
||||||
//column.dataIndex = column.dataIndex.split('.')
|
|
||||||
/*if(){
|
|
||||||
column.dataIndex=
|
|
||||||
}*/
|
|
||||||
})
|
})
|
||||||
return cols
|
return cols
|
||||||
})
|
})
|
||||||
@@ -699,6 +704,19 @@ defineExpose({
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fixed ops column must fully cover neighbors (RTL left / fixed-right) */
|
||||||
|
.ant-table-cell-fix-right,
|
||||||
|
.ant-table-cell-fix-right.ant-table-cell-fix-last-right {
|
||||||
|
background: var(--admin-bg-elevated, var(--admin-bg-surface, #141414)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.is-dark & {
|
||||||
|
.ant-table-cell-fix-right,
|
||||||
|
.ant-table-cell-fix-right.ant-table-cell-fix-last-right {
|
||||||
|
background: var(--admin-bg-elevated, #1f1f1f) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.table-footer {
|
.table-footer {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<!-- relative h-full-->
|
<!-- relative h-full-->
|
||||||
<div class="scrollable-container" id="components-affix-demo-target" ref="containerRef">
|
<div class="scrollable-container" id="components-affix-demo-target" ref="containerRef">
|
||||||
<!--- TODO remove AFFix -->
|
<!--- TODO remove AFFix -->
|
||||||
|
<div class="table-toolbar shrink-0 mb-2">
|
||||||
<AAffix :target="() => containerRef">
|
<AAffix :target="() => containerRef">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
@@ -84,10 +85,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AAffix>
|
</AAffix>
|
||||||
|
</div>
|
||||||
|
<div class="table-body min-h-0 min-w-0 flex-1 overflow-auto">
|
||||||
<ATable
|
<ATable
|
||||||
:columns="_columns"
|
:columns="_columns"
|
||||||
:data-source="data"
|
:data-source="data"
|
||||||
v-bind="_props"
|
v-bind="_props"
|
||||||
|
:scroll="tableScroll"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
@resizeColumn="handleResizeColumn"
|
@resizeColumn="handleResizeColumn"
|
||||||
>
|
>
|
||||||
@@ -100,9 +104,10 @@
|
|||||||
</template>
|
</template>
|
||||||
<!-- <template #loading> sdfsdfsdfsd </template>-->
|
<!-- <template #loading> sdfsdfsdfsd </template>-->
|
||||||
</ATable>
|
</ATable>
|
||||||
|
</div>
|
||||||
<!-- absolute bottom-0 w-full-->
|
<!-- absolute bottom-0 w-full-->
|
||||||
<!-- absolute bottom-0-->
|
<!-- absolute bottom-0-->
|
||||||
<div class="flex items-center justify-between mt-5 w-full">
|
<div class="table-footer flex items-center justify-between mt-5 w-full shrink-0">
|
||||||
<span> {{ totalRecord }} مورد یافت شد </span>
|
<span> {{ totalRecord }} مورد یافت شد </span>
|
||||||
<div class="flex items-center gap-2 justify-between">
|
<div class="flex items-center gap-2 justify-between">
|
||||||
<APagination
|
<APagination
|
||||||
@@ -296,24 +301,9 @@ import {
|
|||||||
availableFilterType,
|
availableFilterType,
|
||||||
MapFilterOpToFaString
|
MapFilterOpToFaString
|
||||||
} from '@core/components/BaseTableOld/hooks/filterOperator'
|
} from '@core/components/BaseTableOld/hooks/filterOperator'
|
||||||
//import { mapColumnTypeFilterToFilterType } from '@core/type/baseTable'
|
|
||||||
//import { useElementSize } from '@vueuse/core'
|
/** Wide enough for dense action icon rows; too-narrow fixed cells bleed into neighbors. */
|
||||||
//import { ContentLoader } from 'vue-content-loader'
|
const DEFAULT_ACTION_WIDTH = 280
|
||||||
//skeleton
|
|
||||||
//const skeletonWidth = ref(null)
|
|
||||||
/*const skeletonWidthSize = reactive(
|
|
||||||
useElementSize(skeletonWidth, { width: 0, height: 0 }, { box: 'border-box' })
|
|
||||||
)*/
|
|
||||||
//const skeletonHeight = 357
|
|
||||||
/*const skeletonHeight = ref(null)
|
|
||||||
const skeletonHeightSize = reactive(
|
|
||||||
useElementSize(skeletonHeight, { width: 0, height: 0 }, { box: 'border-box' })
|
|
||||||
)*/
|
|
||||||
/*
|
|
||||||
watch(skeletonHeightSize, (v) => {
|
|
||||||
console.log('ttt', v)
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bordered?: boolean
|
bordered?: boolean
|
||||||
@@ -331,7 +321,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
driver: 'laravel-sql',
|
driver: 'laravel-sql',
|
||||||
mode: 'pro',
|
mode: 'pro',
|
||||||
columns: () => [],
|
columns: () => [],
|
||||||
scroll: () => ({ x: true }), //y: '100%' //y: 600
|
scroll: () => ({ x: true }),
|
||||||
pageSize: () => [5, 10, 20, 50, 100],
|
pageSize: () => [5, 10, 20, 50, 100],
|
||||||
defaultPageLimit: 10
|
defaultPageLimit: 10
|
||||||
})
|
})
|
||||||
@@ -345,11 +335,28 @@ const filters = ref<Filter[]>([])
|
|||||||
const order = ref<'asc' | 'desc'>('asc')
|
const order = ref<'asc' | 'desc'>('asc')
|
||||||
const orderColumnKey = ref<string | null>(null)
|
const orderColumnKey = ref<string | null>(null)
|
||||||
const containerRef = ref()
|
const containerRef = ref()
|
||||||
//const route = useRoute()
|
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
const _props = computed(() => {
|
const _props = computed(() => {
|
||||||
return omit(props, ['columns', 'pagination', 'dataSource'])
|
return omit(props, ['columns', 'pagination', 'dataSource', 'scroll'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const tableScroll = computed(() => ({
|
||||||
|
x: props.scroll?.x ?? true,
|
||||||
|
...(props.scroll?.y != null ? { y: props.scroll.y } : {})
|
||||||
|
}))
|
||||||
|
|
||||||
|
function isActionColumn(column: BaseTableColumn) {
|
||||||
|
const rawIndex = column.dataIndex
|
||||||
|
const dataIndex = Array.isArray(rawIndex) ? rawIndex.join('.') : String(rawIndex ?? '')
|
||||||
|
const key = column.key != null ? String(column.key) : ''
|
||||||
|
return (
|
||||||
|
column.slot === 'action' ||
|
||||||
|
key === 'action' ||
|
||||||
|
key === 'row_action' ||
|
||||||
|
dataIndex === 'action' ||
|
||||||
|
dataIndex === 'row_action'
|
||||||
|
)
|
||||||
|
}
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'register', data: BaseTableEmit): void
|
(e: 'register', data: BaseTableEmit): void
|
||||||
}>()
|
}>()
|
||||||
@@ -468,15 +475,17 @@ const _columns = computed(() => {
|
|||||||
if (!Object.hasOwn(column, 'align')) {
|
if (!Object.hasOwn(column, 'align')) {
|
||||||
column.align = 'center'
|
column.align = 'center'
|
||||||
}
|
}
|
||||||
|
if (isActionColumn(column)) {
|
||||||
|
if (!Object.hasOwn(column, 'fixed') || column.fixed == null) {
|
||||||
|
column.fixed = 'right'
|
||||||
|
}
|
||||||
|
const w = Number(column.width)
|
||||||
|
if (!Object.hasOwn(column, 'width') || column.width == null || !Number.isFinite(w) || w < 200) {
|
||||||
|
column.width = DEFAULT_ACTION_WIDTH
|
||||||
|
}
|
||||||
|
}
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
column.dataIndex = column.dataIndex.split('.')
|
column.dataIndex = String(column.dataIndex ?? '').split('.')
|
||||||
//if(isArray(column.dataIndex))
|
|
||||||
/*if(!isArray(column.dataIndex)){
|
|
||||||
}*/
|
|
||||||
//column.dataIndex = column.dataIndex.split('.')
|
|
||||||
/*if(){
|
|
||||||
column.dataIndex=
|
|
||||||
}*/
|
|
||||||
})
|
})
|
||||||
return cols
|
return cols
|
||||||
})
|
})
|
||||||
@@ -652,7 +661,6 @@ async function callApi() {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await callApi()
|
await callApi()
|
||||||
emit('register', { reload })
|
emit('register', { reload })
|
||||||
//console.log('router', route.query)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -666,11 +674,26 @@ defineExpose({
|
|||||||
.@{prefix} {
|
.@{prefix} {
|
||||||
@apply h-full max-w-full flex flex-col min-h-0;
|
@apply h-full max-w-full flex flex-col min-h-0;
|
||||||
.scrollable-container {
|
.scrollable-container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.table-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-x: auto;
|
min-width: 0;
|
||||||
overflow-y: auto;
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.table-footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.ant-table-cell-fix-right,
|
||||||
|
.ant-table-cell-fix-right.ant-table-cell-fix-last-right {
|
||||||
|
background: var(--admin-bg-elevated, var(--admin-bg-surface, #141414)) !important;
|
||||||
}
|
}
|
||||||
.ant-table-wrapper {
|
.ant-table-wrapper {
|
||||||
border: 1px solid var(--admin-border);
|
border: 1px solid var(--admin-border);
|
||||||
|
|||||||
@@ -30,17 +30,17 @@
|
|||||||
|
|
||||||
<a-form-item label="شرط اجرا" name="price_condition" :rules="inputRules()">
|
<a-form-item label="شرط اجرا" name="price_condition" :rules="inputRules()">
|
||||||
<a-radio-group v-model:value="form.price_condition">
|
<a-radio-group v-model:value="form.price_condition">
|
||||||
<a-radio value="gte">وقتی مظنه به این عدد یا بالاتر رسید</a-radio>
|
<a-radio value="gte">مساوی یا بیشتر از مظنه هدف</a-radio>
|
||||||
<a-radio value="lte">وقتی مظنه به این عدد یا پایینتر رسید</a-radio>
|
<a-radio value="lte">مساوی یا کمتر از مظنه هدف</a-radio>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item label="مظنه تریگر" name="trigger_price" :rules="inputRulesNumber()">
|
<a-form-item label="مظنه هدف" name="trigger_price" :rules="inputRulesNumber()">
|
||||||
<BasicInput
|
<BasicInput
|
||||||
v-model:value="form.trigger_price"
|
v-model:value="form.trigger_price"
|
||||||
type="price"
|
type="price"
|
||||||
addon-before="ریال"
|
addon-before="ریال"
|
||||||
placeholder="مثلاً ۸۰۲۰۰۰۰۰۰"
|
placeholder="مثلاً 20,000,0000"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
@@ -49,11 +49,11 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item label="اعتبار تا" name="expires_at">
|
<a-form-item label="اعتبار تا" name="expires_at">
|
||||||
<a-date-picker
|
<BaseDatePicker
|
||||||
v-model:value="expiresAtModel"
|
v-model:value="expiresAtModel"
|
||||||
show-time
|
show-time
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
placeholder="تاریخ و ساعت انقضا"
|
placeholder="تاریخ و ساعت انقضا"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -73,6 +73,7 @@ import { computed, ref } from 'vue'
|
|||||||
import type { FormInstance } from 'ant-design-vue'
|
import type { FormInstance } from 'ant-design-vue'
|
||||||
import type { ConditionalOrder } from '@/model/conditionalOrders'
|
import type { ConditionalOrder } from '@/model/conditionalOrders'
|
||||||
import { inputRules, inputRulesNumber } from '@/utils/inputRules'
|
import { inputRules, inputRulesNumber } from '@/utils/inputRules'
|
||||||
|
import BaseDatePicker from '@core/components/BaseDatePicker'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
form: Partial<ConditionalOrder>
|
form: Partial<ConditionalOrder>
|
||||||
@@ -84,8 +85,8 @@ const props = defineProps<{
|
|||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
const expiresAtModel = computed({
|
const expiresAtModel = computed({
|
||||||
get: () => props.form.expires_at ?? undefined,
|
get: () => props.form.expires_at ?? '',
|
||||||
set: (v: string | undefined) => {
|
set: (v: string | null) => {
|
||||||
props.form.expires_at = v || null
|
props.form.expires_at = v || null
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<BaseButton type="primary" size="small" @click="emits('add')">
|
<BaseButton type="primary" size="small" @click="emits('add')">
|
||||||
<span>افزودن تریگر</span>
|
<span>افزودن سفارش شرطی</span>
|
||||||
<BaseIcon icon="vuesax-linear:add" size="16" />
|
<BaseIcon icon="vuesax-linear:add" size="16" />
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,16 +47,16 @@ export const cols: BaseTableColumn[] = [
|
|||||||
key: 'price_condition',
|
key: 'price_condition',
|
||||||
customRender: ({ record }) =>
|
customRender: ({ record }) =>
|
||||||
record.price_condition === 'gte' ? (
|
record.price_condition === 'gte' ? (
|
||||||
<CustomTag>یا بالاتر</CustomTag>
|
<CustomTag>مساوی یا بیشتر</CustomTag>
|
||||||
) : (
|
) : (
|
||||||
<CustomTag>یا پایینتر</CustomTag>
|
<CustomTag>مساوی یا کمتر</CustomTag>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'مظنه تریگر',
|
title: 'مظنه هدف',
|
||||||
dataIndex: 'trigger_price',
|
dataIndex: 'trigger_price',
|
||||||
key: 'trigger_price',
|
key: 'trigger_price',
|
||||||
type: 'Number',
|
type: 'Number'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'مقدار',
|
title: 'مقدار',
|
||||||
|
|||||||
@@ -57,42 +57,31 @@ export const cols: BaseTableColumn[] = [
|
|||||||
type: 'Text'
|
type: 'Text'
|
||||||
},*/
|
},*/
|
||||||
{
|
{
|
||||||
title: 'وضعیت کاربر',
|
title: 'وضعیت',
|
||||||
dataIndex: 'active',
|
dataIndex: 'active',
|
||||||
key: 'active',
|
key: 'status',
|
||||||
type: 'Boolean',
|
type: 'Boolean',
|
||||||
|
width: 132,
|
||||||
|
filterable: false,
|
||||||
|
sortable: false,
|
||||||
customRender: ({ record }) => {
|
customRender: ({ record }) => {
|
||||||
return record.active ? (
|
const userOk = Boolean(record.active)
|
||||||
<div class="flex justify-center">
|
const tradeOk = Boolean(record.trade_active)
|
||||||
<CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
|
return (
|
||||||
فعال
|
<div class="flex flex-col items-center gap-1">
|
||||||
|
<CustomTag
|
||||||
|
textColor={userOk ? '#25855A' : '#C53030'}
|
||||||
|
bgColor={userOk ? '#E3F4E9' : '#FECACA'}
|
||||||
|
class="m-0 font-bold !px-2.5 !py-0 text-xs leading-6 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{userOk ? 'حساب فعال' : 'حساب غیرفعال'}
|
||||||
</CustomTag>
|
</CustomTag>
|
||||||
</div>
|
<CustomTag
|
||||||
) : (
|
textColor={tradeOk ? '#25855A' : '#C53030'}
|
||||||
<div class="flex justify-center">
|
bgColor={tradeOk ? '#E3F4E9' : '#FECACA'}
|
||||||
<CustomTag textColor="#C53030" bgColor="#FECACA" class={'font-bold px-6'}>
|
class="m-0 font-bold !px-2.5 !py-0 text-xs leading-6 whitespace-nowrap"
|
||||||
غیر فعال
|
>
|
||||||
</CustomTag>
|
{tradeOk ? 'سفارش مجاز' : 'سفارش غیرمجاز'}
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'وضعیت سفارش دهی',
|
|
||||||
dataIndex: 'trade_active',
|
|
||||||
key: 'trade_active',
|
|
||||||
type: 'Boolean',
|
|
||||||
customRender: ({ record }) => {
|
|
||||||
return record.trade_active ? (
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
|
|
||||||
مجاز
|
|
||||||
</CustomTag>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<CustomTag textColor="#C53030" bgColor="#FECACA" class={'font-bold px-6'}>
|
|
||||||
غیر مجاز
|
|
||||||
</CustomTag>
|
</CustomTag>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
<template>
|
||||||
|
<div class="co-tab">
|
||||||
|
<div class="co-grid">
|
||||||
|
<article class="co-panel" :class="{ 'co-panel--on': active, 'co-panel--off': !active }">
|
||||||
|
<header class="co-panel__head">
|
||||||
|
<div>
|
||||||
|
<h3 class="co-panel__title">دسترسی سفارش شرطی</h3>
|
||||||
|
<p class="co-panel__key">مجوز این مشتری</p>
|
||||||
|
</div>
|
||||||
|
<span class="co-badge" :class="active ? 'co-badge--on' : 'co-badge--off'">
|
||||||
|
{{ active ? 'مجاز' : 'محدود' }}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="co-panel__help">
|
||||||
|
<div class="co-help-line">
|
||||||
|
<span class="co-help-pill co-help-pill--on">روشن</span>
|
||||||
|
<span>منو در اپ دیده میشود و مشتری میتواند تریگر بسازد.</span>
|
||||||
|
</div>
|
||||||
|
<div class="co-help-line">
|
||||||
|
<span class="co-help-pill co-help-pill--off">خاموش</span>
|
||||||
|
<span>منو مخفی است و ثبت تریگر برای این مشتری بسته میشود.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="co-panel__control">
|
||||||
|
<a-radio-group
|
||||||
|
:value="active ? 'on' : 'off'"
|
||||||
|
button-style="solid"
|
||||||
|
:disabled="activeLoading"
|
||||||
|
class="co-toggle-group"
|
||||||
|
@change="onAccessChange"
|
||||||
|
>
|
||||||
|
<a-radio-button value="on">مجاز</a-radio-button>
|
||||||
|
<a-radio-button value="off">محدود</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
<a-spin v-if="activeLoading" size="small" />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="co-panel" :class="{ 'co-panel--muted': !active }">
|
||||||
|
<header class="co-panel__head">
|
||||||
|
<div>
|
||||||
|
<h3 class="co-panel__title">سقف تریگرهای فعال</h3>
|
||||||
|
<p class="co-panel__key">حداکثر pending همزمان</p>
|
||||||
|
</div>
|
||||||
|
<span class="co-badge co-badge--neutral">{{ limitLabel }}</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p class="co-panel__desc">
|
||||||
|
بعد از اجرا، ناموفق شدن یا انقضای تریگر، یک جای خالی آزاد میشود. خالی = بدون سقف.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="co-panel__control co-panel__control--stack">
|
||||||
|
<div class="co-limit-row">
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="limitDraft"
|
||||||
|
class="co-limit-input"
|
||||||
|
:min="1"
|
||||||
|
:precision="0"
|
||||||
|
:disabled="!active"
|
||||||
|
placeholder="خالی = نامحدود"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
<span class="co-limit-suffix">عدد</span>
|
||||||
|
</div>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
:loading="limitLoading"
|
||||||
|
:disabled="!active || !limitDirty"
|
||||||
|
@click="saveLimit"
|
||||||
|
>
|
||||||
|
ذخیره سقف
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import {
|
||||||
|
changeCustomerConditionalOrderActive,
|
||||||
|
updateCustomerConditionalOrderLimit,
|
||||||
|
} from '@/api/customer'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
customerId: number
|
||||||
|
conditionalOrderActive?: boolean | number | null
|
||||||
|
conditionalOrderLimit?: number | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
updated: [
|
||||||
|
payload: {
|
||||||
|
conditional_order_active?: boolean
|
||||||
|
conditional_order_limit?: number | null
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const activeLoading = ref(false)
|
||||||
|
const limitLoading = ref(false)
|
||||||
|
const limitDraft = ref<number | null>(null)
|
||||||
|
|
||||||
|
const active = computed(() => Boolean(props.conditionalOrderActive))
|
||||||
|
|
||||||
|
const savedLimit = computed(() => {
|
||||||
|
const v = props.conditionalOrderLimit
|
||||||
|
return v != null && Number(v) > 0 ? Number(v) : null
|
||||||
|
})
|
||||||
|
|
||||||
|
const limitLabel = computed(() =>
|
||||||
|
savedLimit.value != null ? `${savedLimit.value} عدد` : 'نامحدود'
|
||||||
|
)
|
||||||
|
|
||||||
|
const draftNormalized = computed(() => {
|
||||||
|
if (limitDraft.value == null || !Number.isFinite(Number(limitDraft.value))) return null
|
||||||
|
return Math.max(1, Math.floor(Number(limitDraft.value)))
|
||||||
|
})
|
||||||
|
|
||||||
|
const limitDirty = computed(() => draftNormalized.value !== savedLimit.value)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.conditionalOrderLimit,
|
||||||
|
(v) => {
|
||||||
|
limitDraft.value = v != null && Number(v) > 0 ? Number(v) : null
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
async function onAccessChange(e: { target?: { value?: string } }) {
|
||||||
|
const wantOn = e?.target?.value === 'on'
|
||||||
|
if (wantOn === active.value || !props.customerId || activeLoading.value) return
|
||||||
|
|
||||||
|
activeLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await changeCustomerConditionalOrderActive(props.customerId)
|
||||||
|
emit('updated', {
|
||||||
|
conditional_order_active: Boolean(
|
||||||
|
res?.conditional_order_active ?? !props.conditionalOrderActive
|
||||||
|
),
|
||||||
|
})
|
||||||
|
message.success('مجوز سفارش شرطی بهروز شد')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
activeLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveLimit() {
|
||||||
|
if (!props.customerId || limitLoading.value || !active.value) return
|
||||||
|
|
||||||
|
limitLoading.value = true
|
||||||
|
try {
|
||||||
|
const limit = draftNormalized.value
|
||||||
|
const res = await updateCustomerConditionalOrderLimit(props.customerId, limit)
|
||||||
|
emit('updated', {
|
||||||
|
conditional_order_limit: res?.conditional_order_limit ?? limit,
|
||||||
|
})
|
||||||
|
message.success('سقف سفارش شرطی ذخیره شد')
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
limitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.co-tab {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-grid {
|
||||||
|
@apply grid w-full grid-cols-1 gap-3 lg:grid-cols-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-panel {
|
||||||
|
@apply flex min-h-52 flex-col rounded-xl border p-4;
|
||||||
|
border-color: var(--admin-border, #303030);
|
||||||
|
background: var(--admin-bg-elevated, var(--admin-surface-muted, #1a1a1a));
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-panel:hover {
|
||||||
|
border-color: color-mix(in srgb, var(--admin-primary, #eab308) 45%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-panel--on {
|
||||||
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, #25855a 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-panel--off {
|
||||||
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, #c53030 28%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-panel--muted {
|
||||||
|
@apply opacity-55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-panel__head {
|
||||||
|
@apply mb-3 flex items-start justify-between gap-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-panel__title {
|
||||||
|
@apply m-0 text-base font-700;
|
||||||
|
color: var(--admin-text, inherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-panel__key {
|
||||||
|
@apply m-0 mt-1 text-xs;
|
||||||
|
color: var(--admin-text-secondary, #9ca3af);
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-panel__desc {
|
||||||
|
@apply m-0 mb-4 text-xs leading-5;
|
||||||
|
color: var(--admin-text-secondary, #9ca3af);
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-panel__help {
|
||||||
|
@apply mb-4 flex flex-col gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-help-line {
|
||||||
|
@apply flex items-start gap-2 text-xs leading-5;
|
||||||
|
color: var(--admin-text-secondary, #9ca3af);
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-help-pill {
|
||||||
|
@apply shrink-0 rounded px-2 py-0.5 text-[11px] font-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-help-pill--on {
|
||||||
|
color: #25855a;
|
||||||
|
background: #e3f4e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-help-pill--off {
|
||||||
|
color: #c53030;
|
||||||
|
background: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-badge {
|
||||||
|
@apply shrink-0 rounded-full px-3 py-1 text-xs font-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-badge--on {
|
||||||
|
color: #25855a;
|
||||||
|
background: #e3f4e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-badge--off {
|
||||||
|
color: #c53030;
|
||||||
|
background: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-badge--neutral {
|
||||||
|
color: var(--admin-text, inherit);
|
||||||
|
background: var(--admin-bg-muted, rgba(255, 255, 255, 0.06));
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-panel__control {
|
||||||
|
@apply mt-auto flex items-center gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-panel__control--stack {
|
||||||
|
@apply flex-col items-stretch gap-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-toggle-group {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-toggle-group :deep(.ant-radio-button-wrapper) {
|
||||||
|
@apply flex-1 text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-limit-row {
|
||||||
|
@apply flex items-center gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-limit-input {
|
||||||
|
@apply w-full flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-limit-suffix {
|
||||||
|
@apply shrink-0 text-sm;
|
||||||
|
color: var(--admin-text-secondary, #9ca3af);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
<MobileDetailSheet v-model:visible="visibleProxy" :item="item" :title="sheetTitle">
|
<MobileDetailSheet v-model:visible="visibleProxy" :item="item" :title="sheetTitle">
|
||||||
<template #default="{ item: row }">
|
<template #default="{ item: row }">
|
||||||
<div class="customer-mobile-sheet">
|
<div class="customer-mobile-sheet">
|
||||||
|
<CustomerPresenceBadge :last-seen-at="row.last_seen_at" />
|
||||||
<p v-if="row.mobile" class="customer-mobile-sheet__mobile" dir="ltr">{{ row.mobile }}</p>
|
<p v-if="row.mobile" class="customer-mobile-sheet__mobile" dir="ltr">{{ row.mobile }}</p>
|
||||||
|
|
||||||
<div class="customer-mobile-sheet__chips">
|
<div class="customer-mobile-sheet__chips">
|
||||||
@@ -100,7 +101,6 @@
|
|||||||
/>
|
/>
|
||||||
<MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" />
|
<MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" />
|
||||||
<MobileDetailRow label="نام معرف" :value="row.referral?.name ?? '-'" />
|
<MobileDetailRow label="نام معرف" :value="row.referral?.name ?? '-'" />
|
||||||
<MobileDetailRow label="آخرین بازدید" :value="lastSeen(row)" value-class="text-xs" />
|
|
||||||
</MobileInfoGrid>
|
</MobileInfoGrid>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -118,13 +118,13 @@ import MobileDetailRow from '@/components/mobile/MobileDetailRow.vue'
|
|||||||
import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue'
|
import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue'
|
||||||
import MobileInfoGrid from '@/components/mobile/MobileInfoGrid.vue'
|
import MobileInfoGrid from '@/components/mobile/MobileInfoGrid.vue'
|
||||||
import CustomerWalletBalancePanel from '@/components/customer/CustomerWalletBalancePanel.vue'
|
import CustomerWalletBalancePanel from '@/components/customer/CustomerWalletBalancePanel.vue'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
|
||||||
import {
|
import {
|
||||||
accountingProviderSetting,
|
accountingProviderSetting,
|
||||||
customerDirectSmsEnabledSetting,
|
customerDirectSmsEnabledSetting,
|
||||||
initAccountingSettingsFromApi
|
initAccountingSettingsFromApi
|
||||||
} from '@/utils/config'
|
} from '@/utils/config'
|
||||||
import { displayAccountingCode } from '@/utils/accountingCode'
|
import { displayAccountingCode } from '@/utils/accountingCode'
|
||||||
|
import CustomerPresenceBadge from '@/pages/menuPages/customers/base/components/CustomerPresenceBadge.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean
|
visible: boolean
|
||||||
@@ -200,10 +200,6 @@ function confirmToggleTrade(row: Customer) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function lastSeen(row: Customer) {
|
|
||||||
return row.last_seen_at ? toJalali(row.last_seen_at) : '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadChargeTargets() {
|
async function loadChargeTargets() {
|
||||||
try {
|
try {
|
||||||
const res = await getChargeTargets()
|
const res = await getChargeTargets()
|
||||||
@@ -260,6 +256,16 @@ watch(
|
|||||||
color: #b91c1c;
|
color: #b91c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.customer-mobile-sheet__chip.is-online {
|
||||||
|
background: rgba(37, 99, 235, 0.14);
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-mobile-sheet__chip.is-muted {
|
||||||
|
background: rgba(107, 114, 128, 0.14);
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
.customer-mobile-sheet__section {
|
.customer-mobile-sheet__section {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<div class="presence-badge" :class="`presence-badge--${kind}`" :title="hint">
|
||||||
|
<span class="presence-badge__glow" aria-hidden="true" />
|
||||||
|
<span class="presence-badge__dot" aria-hidden="true" />
|
||||||
|
<span class="presence-badge__label">{{ label }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { getCustomerPresenceKind } from '@/utils/customerPresence'
|
||||||
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
lastSeenAt?: string | Date | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const kind = computed(() => getCustomerPresenceKind(props.lastSeenAt))
|
||||||
|
|
||||||
|
const label = computed(() => {
|
||||||
|
if (kind.value === 'online') return 'آنلاین'
|
||||||
|
if (kind.value === 'never') return 'وارد نشده'
|
||||||
|
return props.lastSeenAt ? `آخرین بازدید ${toJalali(props.lastSeenAt as string)}` : 'آفلاین'
|
||||||
|
})
|
||||||
|
|
||||||
|
const hint = computed(() => {
|
||||||
|
if (kind.value === 'online') return 'اتصال زنده (SSE) فعال است'
|
||||||
|
if (kind.value === 'never') return 'هنوز وارد اپ نشده'
|
||||||
|
return props.lastSeenAt ? toJalali(props.lastSeenAt as string) : ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.presence-badge {
|
||||||
|
@apply relative inline-flex items-center gap-2 mt-1.5 px-2.5 py-1 rounded-full w-fit max-w-full;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-badge__glow {
|
||||||
|
@apply pointer-events-none absolute inset-0 rounded-full opacity-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-badge__dot {
|
||||||
|
@apply relative h-2 w-2 rounded-full shrink-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-badge__label {
|
||||||
|
@apply text-xs font-700 truncate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-badge--online {
|
||||||
|
background: rgba(34, 197, 94, 0.12);
|
||||||
|
border-color: rgba(34, 197, 94, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-badge--online .presence-badge__glow {
|
||||||
|
opacity: 1;
|
||||||
|
background: radial-gradient(circle at 30% 50%, rgba(34, 197, 94, 0.25), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-badge--online .presence-badge__dot {
|
||||||
|
background: #22c55e;
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.55);
|
||||||
|
animation: presence-badge-pulse 1.6s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-badge--online .presence-badge__label {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-badge--offline {
|
||||||
|
background: rgba(156, 163, 175, 0.12);
|
||||||
|
border-color: rgba(156, 163, 175, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-badge--offline .presence-badge__dot {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-badge--offline .presence-badge__label {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-badge--never {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border-color: rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-badge--never .presence-badge__dot {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-badge--never .presence-badge__label {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes presence-badge-pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.55);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 8px rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.is-dark) .presence-badge--online .presence-badge__label {
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.is-dark) .presence-badge--offline .presence-badge__label {
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.is-dark) .presence-badge--never .presence-badge__label {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div class="presence-cell" :class="`presence-cell--${kind}`">
|
||||||
|
<span class="presence-cell__dot" aria-hidden="true" />
|
||||||
|
<span class="presence-cell__label">{{ label }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { getCustomerPresenceKind } from '@/utils/customerPresence'
|
||||||
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
lastSeenAt?: string | Date | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const kind = computed(() => getCustomerPresenceKind(props.lastSeenAt))
|
||||||
|
|
||||||
|
const label = computed(() => {
|
||||||
|
if (kind.value === 'online') return 'آنلاین'
|
||||||
|
if (kind.value === 'never') return 'وارد نشده'
|
||||||
|
return props.lastSeenAt ? toJalali(props.lastSeenAt as string) : '—'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.presence-cell {
|
||||||
|
@apply flex flex-col items-center justify-center gap-1 py-1 min-w-[5.5rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-cell__dot {
|
||||||
|
@apply relative inline-block h-2.5 w-2.5 rounded-full shrink-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-cell__label {
|
||||||
|
@apply text-xs font-600 text-center leading-snug;
|
||||||
|
max-width: 7.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-cell--online .presence-cell__dot {
|
||||||
|
background: #22c55e;
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.55);
|
||||||
|
animation: presence-pulse 1.6s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-cell--online .presence-cell__label {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-cell--offline .presence-cell__dot {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-cell--offline .presence-cell__label {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-cell--never .presence-cell__dot {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-cell--never .presence-cell__label {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes presence-pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.55);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 8px rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.is-dark) .presence-cell--online .presence-cell__label {
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.is-dark) .presence-cell--offline .presence-cell__label {
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.is-dark) .presence-cell--never .presence-cell__label {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { BaseTableColumn } from '@core/type/baseTable'
|
import type { BaseTableColumn } from '@core/type/baseTable'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
|
||||||
import CustomTag from '@/components/CustomTag.vue'
|
import CustomTag from '@/components/CustomTag.vue'
|
||||||
import { usePrice } from '@core/utils/usePrice'
|
import { usePrice } from '@core/utils/usePrice'
|
||||||
import { getCashBalance } from '@/components/mobile/utils/wallet'
|
import { getCashBalance } from '@/components/mobile/utils/wallet'
|
||||||
import { customerWalletMetalBalanceTitle } from '@/utils/adminMetalLabels'
|
import { customerWalletMetalBalanceTitle } from '@/utils/adminMetalLabels'
|
||||||
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
|
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
|
||||||
import type { AccountingProvider } from '@/utils/config'
|
import type { AccountingProvider } from '@/utils/config'
|
||||||
|
import CustomerPresenceCell from '@/pages/menuPages/customers/base/components/CustomerPresenceCell.vue'
|
||||||
|
|
||||||
export type CustomerMetalColumn = { metal_id: number; label: string; display_unit?: string }
|
export type CustomerMetalColumn = { metal_id: number; label: string; display_unit?: string }
|
||||||
|
|
||||||
@@ -63,24 +63,12 @@ export function buildCustomerColumns(
|
|||||||
key: 'mobile',
|
key: 'mobile',
|
||||||
type: 'Number'
|
type: 'Number'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'نوع کاربر',
|
|
||||||
dataIndex: 'type_str',
|
|
||||||
key: 'type_str',
|
|
||||||
type: 'Text'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'کد معرف',
|
title: 'کد معرف',
|
||||||
dataIndex: 'referral_id',
|
dataIndex: 'referral_id',
|
||||||
key: 'referral_id',
|
key: 'referral_id',
|
||||||
type: 'Text'
|
type: 'Text'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'نام معرف کاربر',
|
|
||||||
dataIndex: 'referral.name',
|
|
||||||
key: 'referral.name',
|
|
||||||
type: 'Text'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'موجودی ریالی (ریال)',
|
title: 'موجودی ریالی (ریال)',
|
||||||
dataIndex: 'wallet.cash',
|
dataIndex: 'wallet.cash',
|
||||||
@@ -122,53 +110,42 @@ export function buildCustomerColumns(
|
|||||||
return [
|
return [
|
||||||
...baseColumns,
|
...baseColumns,
|
||||||
...metalColumns,
|
...metalColumns,
|
||||||
/* {
|
{
|
||||||
title: 'اخرین بازدید',
|
title: 'آخرین بازدید',
|
||||||
dataIndex: 'last_seen_at',
|
dataIndex: 'last_seen_at',
|
||||||
key: 'last_seen_at',
|
key: 'last_seen_at',
|
||||||
type: 'DateTime',
|
type: 'DateTime',
|
||||||
customRender(data) {
|
width: 130,
|
||||||
return toJalali(data.record.last_seen_at)
|
filterable: false,
|
||||||
}
|
sortable: true,
|
||||||
},*/
|
customRender: ({ record }) => <CustomerPresenceCell lastSeenAt={record.last_seen_at} />
|
||||||
{
|
|
||||||
title: 'وضعیت کاربر',
|
|
||||||
dataIndex: 'active',
|
|
||||||
key: 'active',
|
|
||||||
type: 'Boolean',
|
|
||||||
customRender: ({ record }) => {
|
|
||||||
return record.active ? (
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
|
|
||||||
فعال
|
|
||||||
</CustomTag>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<CustomTag textColor="#C53030" bgColor="#FECACA" class={'font-bold px-6'}>
|
|
||||||
غیر فعال
|
|
||||||
</CustomTag>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'وضعیت سفارش دهی',
|
title: 'وضعیت',
|
||||||
dataIndex: 'trade_active',
|
dataIndex: 'active',
|
||||||
key: 'trade_active',
|
key: 'status',
|
||||||
sortable: false,
|
|
||||||
type: 'Boolean',
|
type: 'Boolean',
|
||||||
|
width: 132,
|
||||||
|
filterable: false,
|
||||||
|
sortable: false,
|
||||||
customRender: ({ record }) => {
|
customRender: ({ record }) => {
|
||||||
return record.trade_active ? (
|
const userOk = Boolean(record.active)
|
||||||
<div class="flex justify-center">
|
const tradeOk = Boolean(record.trade_active)
|
||||||
<CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
|
return (
|
||||||
مجاز
|
<div class="flex flex-col items-center gap-1">
|
||||||
|
<CustomTag
|
||||||
|
textColor={userOk ? '#25855A' : '#C53030'}
|
||||||
|
bgColor={userOk ? '#E3F4E9' : '#FECACA'}
|
||||||
|
class="m-0 font-bold !px-2.5 !py-0 text-xs leading-6 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{userOk ? 'حساب فعال' : 'حساب غیرفعال'}
|
||||||
</CustomTag>
|
</CustomTag>
|
||||||
</div>
|
<CustomTag
|
||||||
) : (
|
textColor={tradeOk ? '#25855A' : '#C53030'}
|
||||||
<div class="flex justify-center">
|
bgColor={tradeOk ? '#E3F4E9' : '#FECACA'}
|
||||||
<CustomTag textColor="#C53030" bgColor="#FECACA" class={'font-bold px-6'}>
|
class="m-0 font-bold !px-2.5 !py-0 text-xs leading-6 whitespace-nowrap"
|
||||||
غیر مجاز
|
>
|
||||||
|
{tradeOk ? 'سفارش مجاز' : 'سفارش غیرمجاز'}
|
||||||
</CustomTag>
|
</CustomTag>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -118,6 +118,7 @@
|
|||||||
<div class="admin-stat-muted rounded-lg w-full flex flex-col gap-3 p-3 md:flex-row md:justify-between">
|
<div class="admin-stat-muted rounded-lg w-full flex flex-col gap-3 p-3 md:flex-row md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">{{ userInfo.name }}</div>
|
<div class="font-bold">{{ userInfo.name }}</div>
|
||||||
|
<CustomerPresenceBadge :last-seen-at="userInfo?.last_seen_at" />
|
||||||
<div class="mt-1">{{ userInfo?.mobile }}</div>
|
<div class="mt-1">{{ userInfo?.mobile }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-center items-center pl-3">
|
<div class="flex flex-col justify-center items-center pl-3">
|
||||||
@@ -178,7 +179,6 @@
|
|||||||
title="حداکثر معرفی"
|
title="حداکثر معرفی"
|
||||||
:value="userInfo?.add_user_limit ? `${userInfo?.add_user_limit} نفر` : '_'"
|
:value="userInfo?.add_user_limit ? `${userInfo?.add_user_limit} نفر` : '_'"
|
||||||
/>
|
/>
|
||||||
<BasicInfoItem title="آخرین بازدید" :value="toJalali(userInfo?.last_seen_at)" />
|
|
||||||
<BasicInfoItem title="وضعیت سفارش دهی" class="col-span-2 md:col-span-1">
|
<BasicInfoItem title="وضعیت سفارش دهی" class="col-span-2 md:col-span-1">
|
||||||
<CustomTag
|
<CustomTag
|
||||||
:textColor="userInfo?.trade_active === 1 ? '#25855A' : '#C53030'"
|
:textColor="userInfo?.trade_active === 1 ? '#25855A' : '#C53030'"
|
||||||
@@ -188,41 +188,6 @@
|
|||||||
{{ userInfo?.trade_active === 1 ? 'مجاز' : 'غیر مجاز' }}
|
{{ userInfo?.trade_active === 1 ? 'مجاز' : 'غیر مجاز' }}
|
||||||
</CustomTag>
|
</CustomTag>
|
||||||
</BasicInfoItem>
|
</BasicInfoItem>
|
||||||
<template v-if="conditionalOrderEnabled">
|
|
||||||
<BasicInfoItem title="سفارش شرطی" class="col-span-2 md:col-span-1">
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<CustomTag
|
|
||||||
:textColor="userInfo?.conditional_order_active ? '#25855A' : '#C53030'"
|
|
||||||
:bgColor="userInfo?.conditional_order_active ? '#E3F4E9' : '#FECACA'"
|
|
||||||
class="font-bold px-4"
|
|
||||||
>
|
|
||||||
{{ userInfo?.conditional_order_active ? 'مجاز' : 'غیر مجاز' }}
|
|
||||||
</CustomTag>
|
|
||||||
<BaseButton
|
|
||||||
size="small"
|
|
||||||
type="default"
|
|
||||||
:loading="conditionalOrderLoading"
|
|
||||||
@click="toggleConditionalOrderActive"
|
|
||||||
>
|
|
||||||
{{ userInfo?.conditional_order_active ? 'محدود کردن' : 'مجاز کردن' }}
|
|
||||||
</BaseButton>
|
|
||||||
</div>
|
|
||||||
</BasicInfoItem>
|
|
||||||
<BasicInfoItem title="سقف سفارش شرطی" class="col-span-2 md:col-span-1">
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<span class="font-bold">
|
|
||||||
{{
|
|
||||||
userInfo?.conditional_order_limit
|
|
||||||
? `${userInfo.conditional_order_limit} عدد`
|
|
||||||
: 'نامحدود'
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<BaseButton size="small" type="default" @click="editConditionalOrderLimit">
|
|
||||||
ویرایش سقف
|
|
||||||
</BaseButton>
|
|
||||||
</div>
|
|
||||||
</BasicInfoItem>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="userInfo?.id && isMobile" class="mt-2 space-y-2">
|
<div v-if="userInfo?.id && isMobile" class="mt-2 space-y-2">
|
||||||
@@ -278,6 +243,19 @@
|
|||||||
<a-tab-pane key="قلک ها" tab="قلک ها">
|
<a-tab-pane key="قلک ها" tab="قلک ها">
|
||||||
<FundsTable ref="fundTable" :customerId="userInfo?.id" />
|
<FundsTable ref="fundTable" :customerId="userInfo?.id" />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
<a-tab-pane
|
||||||
|
v-if="conditionalOrderEnabled"
|
||||||
|
key="سفارش شرطی"
|
||||||
|
tab="سفارش شرطی"
|
||||||
|
>
|
||||||
|
<CustomerConditionalOrderTab
|
||||||
|
v-if="userInfo?.id"
|
||||||
|
:customer-id="userInfo.id"
|
||||||
|
:conditional-order-active="userInfo.conditional_order_active"
|
||||||
|
:conditional-order-limit="userInfo.conditional_order_limit"
|
||||||
|
@updated="onConditionalOrderUpdated"
|
||||||
|
/>
|
||||||
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</BasicActionSheet>
|
</BasicActionSheet>
|
||||||
<WalletChargeModal
|
<WalletChargeModal
|
||||||
@@ -314,15 +292,17 @@ import { confirmDeleteItem } from '@/utils/confirmModal'
|
|||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
import type { Item } from '@/model/Item'
|
import type { Item } from '@/model/Item'
|
||||||
import type { Customer } from '@/model/customer'
|
import type { Customer } from '@/model/customer'
|
||||||
import { createCustomer, deleteCustomer, updateCustomer, changeCustomerConditionalOrderActive, updateCustomerConditionalOrderLimit } from '@/api/customer'
|
import { createCustomer, deleteCustomer, getCustomer, updateCustomer } from '@/api/customer'
|
||||||
import customers from '@/router/routes/customers'
|
import customers from '@/router/routes/customers'
|
||||||
import { usePrice } from '@core/utils/usePrice'
|
import { usePrice } from '@core/utils/usePrice'
|
||||||
import jalaliday from 'jalaliday'
|
import jalaliday from 'jalaliday'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import ordersTbl from '@/pages/menuPages/orders/components/table.vue'
|
import ordersTbl from '@/pages/menuPages/orders/components/table.vue'
|
||||||
import transactionsTbl from '@/pages/menuPages/transactions/components/table.vue'
|
import transactionsTbl from '@/pages/menuPages/transactions/components/table.vue'
|
||||||
import smsTbl from '@/pages/menuPages/sms/components/table.vue'
|
import smsTbl from '@/pages/menuPages/sms/components/table.vue'
|
||||||
import FundsTable from '@/pages/menuPages/customers/base/components/fundsTable.vue'
|
import FundsTable from '@/pages/menuPages/customers/base/components/fundsTable.vue'
|
||||||
import ReferralsTable from '@/pages/menuPages/customers/base/components/referralsTable.vue'
|
import ReferralsTable from '@/pages/menuPages/customers/base/components/referralsTable.vue'
|
||||||
|
import CustomerConditionalOrderTab from '@/pages/menuPages/customers/base/components/CustomerConditionalOrderTab.vue'
|
||||||
import type { Order } from '@/model/order'
|
import type { Order } from '@/model/order'
|
||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
@@ -340,10 +320,9 @@ import {
|
|||||||
initWalletSettingsFromApi
|
initWalletSettingsFromApi
|
||||||
} from '@/utils/config'
|
} from '@/utils/config'
|
||||||
import { displayAccountingCode, isValidAccountingCode } from '@/utils/accountingCode'
|
import { displayAccountingCode, isValidAccountingCode } from '@/utils/accountingCode'
|
||||||
|
import CustomerPresenceBadge from '@/pages/menuPages/customers/base/components/CustomerPresenceBadge.vue'
|
||||||
import { syncCustomerWalletFromPargar, type PargarSyncWalletResult } from '@/api/pargar'
|
import { syncCustomerWalletFromPargar, type PargarSyncWalletResult } from '@/api/pargar'
|
||||||
import PargarWalletSyncAction from './components/PargarWalletSyncAction.vue'
|
import PargarWalletSyncAction from './components/PargarWalletSyncAction.vue'
|
||||||
import { MODAL_ABOVE_ACTION_SHEET_Z_INDEX } from '@/constants/overlayZIndex'
|
|
||||||
import { Modal } from 'ant-design-vue'
|
|
||||||
|
|
||||||
const fnameHelp: DashboardHelp = {
|
const fnameHelp: DashboardHelp = {
|
||||||
description: 'نام کوچک مشتری که در اپ و پنل نمایش داده میشود.',
|
description: 'نام کوچک مشتری که در اپ و پنل نمایش داده میشود.',
|
||||||
@@ -576,12 +555,42 @@ const resetData = () => {
|
|||||||
const deleteItem = (id: number) => confirmDeleteItem('کاربر', id, deleteCustomer, refreshTable)
|
const deleteItem = (id: number) => confirmDeleteItem('کاربر', id, deleteCustomer, refreshTable)
|
||||||
|
|
||||||
//define selects
|
//define selects
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
async function openInfoFromQuery() {
|
||||||
|
const raw = route.query.infoId
|
||||||
|
const id = Number(Array.isArray(raw) ? raw[0] : raw)
|
||||||
|
if (!Number.isFinite(id) || id <= 0) return
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const customer = await getCustomer(id)
|
||||||
|
if (customer) await info(customer)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
message.error('بارگذاری مشخصات مشتری ممکن نیست')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
const nextQuery = { ...route.query }
|
||||||
|
delete nextQuery.infoId
|
||||||
|
await router.replace({ query: nextQuery })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await Promise.allSettled([initAccountingSettingsFromApi(), initWalletSettingsFromApi()])
|
await Promise.allSettled([initAccountingSettingsFromApi(), initWalletSettingsFromApi()])
|
||||||
loading.value = false
|
loading.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.infoId,
|
||||||
|
() => {
|
||||||
|
void openInfoFromQuery()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
//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)
|
||||||
@@ -603,57 +612,20 @@ const accountingCodeVisible = computed(() => {
|
|||||||
return provider === 'pargar' || provider === 'tahesab'
|
return provider === 'pargar' || provider === 'tahesab'
|
||||||
})
|
})
|
||||||
|
|
||||||
const conditionalOrderEnabled = conditionalOrderEnabledSetting
|
const conditionalOrderEnabled = computed(() => conditionalOrderEnabledSetting.value)
|
||||||
const conditionalOrderLoading = ref(false)
|
|
||||||
|
|
||||||
async function toggleConditionalOrderActive() {
|
function onConditionalOrderUpdated(payload: {
|
||||||
if (!userInfo.value?.id || conditionalOrderLoading.value) return
|
conditional_order_active?: boolean
|
||||||
conditionalOrderLoading.value = true
|
conditional_order_limit?: number | null
|
||||||
try {
|
}) {
|
||||||
const res = await changeCustomerConditionalOrderActive(userInfo.value.id)
|
if (!userInfo.value) return
|
||||||
userInfo.value.conditional_order_active = Boolean(
|
if (payload.conditional_order_active !== undefined) {
|
||||||
res?.conditional_order_active ?? !userInfo.value.conditional_order_active
|
userInfo.value.conditional_order_active = payload.conditional_order_active
|
||||||
)
|
|
||||||
refreshTable()
|
|
||||||
message.success('مجوز سفارش شرطی بهروز شد')
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
} finally {
|
|
||||||
conditionalOrderLoading.value = false
|
|
||||||
}
|
}
|
||||||
}
|
if (payload.conditional_order_limit !== undefined) {
|
||||||
|
userInfo.value.conditional_order_limit = payload.conditional_order_limit
|
||||||
function editConditionalOrderLimit() {
|
}
|
||||||
if (!userInfo.value?.id) return
|
refreshTable()
|
||||||
const current = userInfo.value.conditional_order_limit
|
|
||||||
Modal.confirm({
|
|
||||||
title: 'سقف تعداد سفارش شرطی',
|
|
||||||
content: `برای «${userInfo.value.name || userInfo.value.id}» عدد سقف را وارد کنید (خالی = نامحدود). فعلی: ${
|
|
||||||
current ? current : 'نامحدود'
|
|
||||||
}`,
|
|
||||||
okText: 'ذخیره',
|
|
||||||
cancelText: 'انصراف',
|
|
||||||
centered: true,
|
|
||||||
zIndex: MODAL_ABOVE_ACTION_SHEET_Z_INDEX,
|
|
||||||
onOk: async () => {
|
|
||||||
const raw = window.prompt(
|
|
||||||
'سقف تعداد تریگر pending (خالی = نامحدود):',
|
|
||||||
current != null ? String(current) : ''
|
|
||||||
)
|
|
||||||
if (raw === null) return
|
|
||||||
const trimmed = raw.trim()
|
|
||||||
const limit = trimmed === '' ? null : Number(trimmed)
|
|
||||||
if (limit !== null && (!Number.isFinite(limit) || limit < 1)) {
|
|
||||||
message.error('عدد نامعتبر است')
|
|
||||||
return Promise.reject()
|
|
||||||
}
|
|
||||||
const res = await updateCustomerConditionalOrderLimit(userInfo.value!.id, limit)
|
|
||||||
userInfo.value!.conditional_order_limit =
|
|
||||||
res?.conditional_order_limit ?? limit
|
|
||||||
refreshTable()
|
|
||||||
message.success('سقف سفارش شرطی ذخیره شد')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountingCodeValue = computed(() => {
|
const accountingCodeValue = computed(() => {
|
||||||
|
|||||||
@@ -17,25 +17,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
import BasicMenu from '@/components/BasicMenu/index.vue'
|
import BasicMenu from '@/components/BasicMenu/index.vue'
|
||||||
import { useRouterStore } from '@core/stores/router'
|
import { useRouterStore } from '@core/stores/router'
|
||||||
import { get } from 'lodash'
|
import { get } from 'lodash'
|
||||||
//import Nature from '@/pages/menuPages/setting/limitedOrder/index.vue'
|
|
||||||
//s import test from './limitedOrder'
|
|
||||||
//import Budget from '@/pages/menuPages/setting/budget/index.vue'
|
|
||||||
//import ProjectType from '@/pages/menuPages/setting/projectType/index.vue'
|
|
||||||
//import DocumentType from '@/pages/menuPages/setting/documentType/index.vue'
|
|
||||||
// import Management from '@/pages/menuPages/setting/management/index.vue'
|
|
||||||
//import Accessibility from '@/pages/menuPages/setting/accessibility/index.vue'
|
|
||||||
//import SubmitProcess from '@/pages/menuPages/setting/submitProcess/index.vue'
|
|
||||||
//import documentGroup from '@/pages/menuPages/setting/documentGroup/index.vue'
|
|
||||||
//import userType from '@/pages/menuPages/setting/userType/index.vue'
|
|
||||||
|
|
||||||
const routerStore = useRouterStore()
|
const routerStore = useRouterStore()
|
||||||
const menu = computed(() => {
|
const menu = computed(() => {
|
||||||
const item = routerStore.routes.find((item) => item.name === 'customers')
|
const item = routerStore.routes.find((item) => item.name === 'customers')
|
||||||
//console.log('aaaa', get(item, 'children[0].children', []))
|
const children = get(item, 'children[0].children', []) as Array<{ meta?: { order?: number } }>
|
||||||
return get(item, 'children[0].children', [])
|
return [...children].sort(
|
||||||
|
(a, b) => (a.meta?.order ?? 99) - (b.meta?.order ?? 99)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,642 @@
|
|||||||
|
<template>
|
||||||
|
<div class="online-page h-full min-h-0 flex flex-col overflow-hidden">
|
||||||
|
<div class="online-page__hero shrink-0 mb-4">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="online-page__title m-0">کاربران آنلاین</h2>
|
||||||
|
<p class="online-page__sub m-0 mt-1">
|
||||||
|
کسانی که الان اپ باز است و اتصال قیمت (SSE) فعال دارند
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="online-page__count">
|
||||||
|
<span class="online-page__count-dot" />
|
||||||
|
<span class="font-700">{{ count }}</span>
|
||||||
|
<span class="text-sm opacity-80">نفر آنلاین</span>
|
||||||
|
</div>
|
||||||
|
<a-button type="primary" size="small" :loading="loading" @click="load">
|
||||||
|
بهروزرسانی
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :spinning="loading" class="flex-1 min-h-0">
|
||||||
|
<div v-if="!loading && !rows.length" class="online-page__empty">
|
||||||
|
<span class="online-page__empty-dot" />
|
||||||
|
<p class="m-0 mt-3 font-600">الان کسی آنلاین نیست</p>
|
||||||
|
<p class="m-0 mt-1 text-sm opacity-70">با باز شدن اپ توسط مشتری، اینجا ظاهر میشود</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="online-page__grid overflow-y-auto min-h-0 pb-4">
|
||||||
|
<article
|
||||||
|
v-for="row in rows"
|
||||||
|
:key="row.id"
|
||||||
|
class="online-card"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="openSheet(row)"
|
||||||
|
@keydown.enter.prevent="openSheet(row)"
|
||||||
|
>
|
||||||
|
<div class="online-card__top">
|
||||||
|
<span class="online-card__pulse" />
|
||||||
|
<span class="online-card__badge">آنلاین</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="online-card__name m-0">{{ row.name || '—' }}</h3>
|
||||||
|
<p class="online-card__mobile m-0" dir="ltr">{{ row.mobile || '—' }}</p>
|
||||||
|
<div class="online-card__meta">
|
||||||
|
<span>کد {{ row.id }}</span>
|
||||||
|
<span>{{ formatSeen(row.last_seen_at) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="online-card__actions" @click.stop>
|
||||||
|
<a-tooltip placement="bottom">
|
||||||
|
<template #title><span>بایگانی</span></template>
|
||||||
|
<a-popconfirm
|
||||||
|
title="آیا مشتری را بایگانی می کنید؟"
|
||||||
|
ok-text="تایید"
|
||||||
|
cancel-text="رد"
|
||||||
|
placement="topLeft"
|
||||||
|
@confirm="archiveFunction(row.id)"
|
||||||
|
>
|
||||||
|
<Icon class="cursor-pointer" icon="vuesax-linear:archive-tick" color="#e3bf00" />
|
||||||
|
</a-popconfirm>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip placement="bottom" class="cursor-pointer" @click="changeCustomerActivationFunction(row.id)">
|
||||||
|
<template #title>
|
||||||
|
<span>{{ row.active ? 'غیرفعال کردن حساب کاربری' : 'فعال کردن حساب کاربری' }}</span>
|
||||||
|
</template>
|
||||||
|
<Icon
|
||||||
|
:icon="row.active ? 'vuesax-linear:user-tick' : 'vuesax-linear:user-remove'"
|
||||||
|
:color="row.active ? '#54AD06' : '#ad0627'"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip placement="bottom" class="cursor-pointer" @click="openWalletCharge(row)">
|
||||||
|
<template #title><span>شارژ کیف پول</span></template>
|
||||||
|
<Icon icon="vuesax-linear:wallet-add" color="#9333ea" />
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip
|
||||||
|
v-if="tahesabEnabled && !isValidAccountingCode(row.moshtari_code)"
|
||||||
|
placement="bottom"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="openTahesabRegister(row)"
|
||||||
|
>
|
||||||
|
<template #title><span>در تهحساب ثبت نشده — کلیک برای ثبت</span></template>
|
||||||
|
<Icon icon="vuesax-linear:profile-add" color="#CA8A04" />
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip
|
||||||
|
v-if="pargarEnabled && !isValidAccountingCode(row.pargar_account_id)"
|
||||||
|
placement="bottom"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="openPargarRegister(row)"
|
||||||
|
>
|
||||||
|
<template #title><span>در پرگار ثبت نشده — کلیک برای ثبت</span></template>
|
||||||
|
<Icon icon="vuesax-linear:profile-add" color="#7C3AED" />
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip
|
||||||
|
v-if="accountingEnabled"
|
||||||
|
placement="bottom"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="openAccountingCodeEdit(row)"
|
||||||
|
>
|
||||||
|
<template #title><span>ویرایش کد حسابداری</span></template>
|
||||||
|
<Icon icon="vuesax-linear:card-edit" color="#0d9488" />
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip placement="bottom" class="cursor-pointer" @click="goInfo(row)">
|
||||||
|
<template #title><span>مشخصات</span></template>
|
||||||
|
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip placement="bottom" class="cursor-pointer" @click="openEdit(row)">
|
||||||
|
<template #title><span>ویرایش</span></template>
|
||||||
|
<Icon icon="vuesax-linear:edit" color="#4572E9" />
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip
|
||||||
|
placement="bottom"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="changeCustomerOrderActivationFunction(row.id)"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<span>{{ row.trade_active ? 'غیر مجاز کردن سفارش دهی' : 'مجاز کردن سفارش دهی' }}</span>
|
||||||
|
</template>
|
||||||
|
<Icon
|
||||||
|
:icon="row.trade_active === 1 ? 'vuesax-linear:box-tick' : 'vuesax-linear:box-remove'"
|
||||||
|
:color="row.trade_active === 1 ? '#54AD06' : '#ad0627'"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip
|
||||||
|
v-if="customerDirectSmsEnabled"
|
||||||
|
placement="bottom"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="openDirectSms(row)"
|
||||||
|
>
|
||||||
|
<template #title><span>ارسال پیامک</span></template>
|
||||||
|
<Icon icon="vuesax-linear:sms" color="#0ea5e9" />
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip
|
||||||
|
v-if="!row.is_legal"
|
||||||
|
placement="bottom"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="openConfirmUser(row)"
|
||||||
|
>
|
||||||
|
<template #title><span>تایید کاربر</span></template>
|
||||||
|
<Icon icon="vuesax-linear:user-search" color="#0d9488" />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
|
||||||
|
<CustomerMobileDetailSheet
|
||||||
|
v-model:visible="sheetVisible"
|
||||||
|
:item="selectedItem"
|
||||||
|
@info="goInfo"
|
||||||
|
@edit="openEdit"
|
||||||
|
@archive="handleArchive"
|
||||||
|
@toggleActive="(item) => changeCustomerActivationFunction(item.id)"
|
||||||
|
@chargeWallet="openWalletCharge"
|
||||||
|
@toggleTrade="(item) => changeCustomerOrderActivationFunction(item.id)"
|
||||||
|
@confirmUser="openConfirmUser"
|
||||||
|
@sendSms="openDirectSms"
|
||||||
|
@editAccounting="openAccountingCodeEdit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<WalletChargeModal
|
||||||
|
v-model:visible="showWalletCharge"
|
||||||
|
:customer-id="chargeCustomerId"
|
||||||
|
@success="load"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CustomerDirectSmsModal v-model:visible="directSmsVisible" :customer="directSmsCustomer" />
|
||||||
|
|
||||||
|
<CustomerAccountingCodeModal
|
||||||
|
v-model:visible="accountingCodeVisible"
|
||||||
|
:customer="accountingCodeCustomer"
|
||||||
|
:provider="accountingProviderSetting"
|
||||||
|
@success="load"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BasicModal
|
||||||
|
v-model:visible="confirmVisible"
|
||||||
|
:width="isMobile ? '92vw' : '50vw'"
|
||||||
|
title="تایید مشتری"
|
||||||
|
@ok="confirmUserFunction"
|
||||||
|
ok-text="تایید"
|
||||||
|
:loading="actionLoading"
|
||||||
|
@close="confirmVisible = false"
|
||||||
|
>
|
||||||
|
<div class="mt-[-10px] mb-10 font-bold">آیا از اطلاعات وارد شده اطمینان دارید؟</div>
|
||||||
|
<div class="grid grid-cols-4 gap-2">
|
||||||
|
<BasicInfoItem title="نام و نام خانوادگی" :value="confirmInfo?.name" />
|
||||||
|
<BasicInfoItem title="شماره همراه" :value="confirmInfo?.mobile" />
|
||||||
|
<BasicInfoItem
|
||||||
|
title="تاریخ تولد"
|
||||||
|
:value="confirmInfo?.birth_date ? toJalaliNoTime(confirmInfo?.birth_date) : '_'"
|
||||||
|
/>
|
||||||
|
<BasicInfoItem title="شماره تلفن" :value="confirmInfo?.phone_number ?? '_'" />
|
||||||
|
<BasicInfoItem title="نام استان" :value="confirmInfo?.province" />
|
||||||
|
<BasicInfoItem title="نام شهر" :value="confirmInfo?.city" />
|
||||||
|
<BasicInfoItem title="شماره ملی" :value="confirmInfo?.n_code" />
|
||||||
|
</div>
|
||||||
|
</BasicModal>
|
||||||
|
|
||||||
|
<BasicActionSheet
|
||||||
|
v-model:visible="editVisible"
|
||||||
|
title="ویرایش کاربر"
|
||||||
|
modal-width="55rem"
|
||||||
|
:loading="actionLoading"
|
||||||
|
:showButton="true"
|
||||||
|
:showCancel="true"
|
||||||
|
:showOk="true"
|
||||||
|
ok-text="ثبت"
|
||||||
|
cancel-text="انصراف"
|
||||||
|
@ok="submitEdit"
|
||||||
|
@close="editVisible = false"
|
||||||
|
>
|
||||||
|
<a-form :model="editForm" ref="editFormRef" autocomplete="off">
|
||||||
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
|
<a-form-item label="نام" name="fname" :rules="inputRules()">
|
||||||
|
<BasicInput v-model:value="editForm.fname" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="نام خانوادگی" name="lname" :rules="inputRules()">
|
||||||
|
<BasicInput v-model:value="editForm.lname" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="شماره همراه" name="mobile" :rules="inputLenRules(11)">
|
||||||
|
<BasicInput v-model:value="editForm.mobile" type="number" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="پسورد" name="password">
|
||||||
|
<BasicInput is-password v-model:value="editForm.password" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="تاریخ تولد" name="birth_date">
|
||||||
|
<BaseDatePicker v-model:value="editForm.birth_date" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="شماره تلفن" name="phone_number">
|
||||||
|
<BasicInput v-model:value="editForm.phone_number" type="number" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="نام استان" name="province">
|
||||||
|
<BasicInput v-model:value="editForm.province" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="نام شهر" name="city">
|
||||||
|
<BasicInput v-model:value="editForm.city" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="شماره ملی" name="n_code">
|
||||||
|
<BasicInput v-model:value="editForm.n_code" />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</BasicActionSheet>
|
||||||
|
|
||||||
|
<TahesabRetryModal
|
||||||
|
v-if="tahesabEnabled"
|
||||||
|
v-model="tahesabRegisterVisible"
|
||||||
|
mode="register-customer"
|
||||||
|
:customer-id="tahesabCustomer?.id"
|
||||||
|
:customer-name="tahesabCustomer?.name"
|
||||||
|
@success="load"
|
||||||
|
/>
|
||||||
|
<PargarRetryModal
|
||||||
|
v-if="pargarEnabled"
|
||||||
|
v-model="pargarRegisterVisible"
|
||||||
|
mode="register-customer"
|
||||||
|
:customer-id="pargarCustomer?.id"
|
||||||
|
:customer-name="pargarCustomer?.name"
|
||||||
|
@success="load"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
|
import type { FormInstance } from 'ant-design-vue'
|
||||||
|
import { message, Modal } from 'ant-design-vue'
|
||||||
|
import {
|
||||||
|
changeCustomerActivation,
|
||||||
|
changeCustomerArchive,
|
||||||
|
changeCustomerOrderActivation,
|
||||||
|
confirmUser,
|
||||||
|
getOnlineCustomers,
|
||||||
|
updateCustomer
|
||||||
|
} from '@/api/customer'
|
||||||
|
import type { Customer } from '@/model/customer'
|
||||||
|
import { toJalali, toJalaliNoTime } from '@core/utils/useDataTime'
|
||||||
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
|
import BasicActionSheet from '@/components/BasicActionSheet.vue'
|
||||||
|
import { isMobile } from '@core/utils/is'
|
||||||
|
import { MODAL_ABOVE_ACTION_SHEET_Z_INDEX } from '@/constants/overlayZIndex'
|
||||||
|
import { inputLenRules, inputRules } from '@/utils/inputRules'
|
||||||
|
import CustomerMobileDetailSheet from '@/pages/menuPages/customers/base/components/CustomerMobileDetailSheet.vue'
|
||||||
|
import WalletChargeModal from '@/pages/menuPages/customers/base/components/WalletChargeModal.vue'
|
||||||
|
import CustomerDirectSmsModal from '@/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue'
|
||||||
|
import CustomerAccountingCodeModal from '@/pages/menuPages/customers/base/components/CustomerAccountingCodeModal.vue'
|
||||||
|
import TahesabRetryModal from '@/components/TahesabRetryModal.vue'
|
||||||
|
import PargarRetryModal from '@/components/PargarRetryModal.vue'
|
||||||
|
import {
|
||||||
|
accountingProviderSetting,
|
||||||
|
customerDirectSmsEnabledSetting,
|
||||||
|
initAccountingSettingsFromApi,
|
||||||
|
initWalletSettingsFromApi,
|
||||||
|
pargarEnabledSetting,
|
||||||
|
tahesabEnabledSetting
|
||||||
|
} from '@/utils/config'
|
||||||
|
import { isValidAccountingCode } from '@/utils/accountingCode'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(false)
|
||||||
|
const actionLoading = ref(false)
|
||||||
|
const rows = ref<Customer[]>([])
|
||||||
|
const count = ref(0)
|
||||||
|
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
const sheetVisible = ref(false)
|
||||||
|
const selectedItem = ref<Customer | null>(null)
|
||||||
|
|
||||||
|
const showWalletCharge = ref(false)
|
||||||
|
const chargeCustomerId = ref<number>()
|
||||||
|
|
||||||
|
const directSmsVisible = ref(false)
|
||||||
|
const directSmsCustomer = ref<Customer | null>(null)
|
||||||
|
|
||||||
|
const accountingCodeVisible = ref(false)
|
||||||
|
const accountingCodeCustomer = ref<Customer | null>(null)
|
||||||
|
|
||||||
|
const confirmVisible = ref(false)
|
||||||
|
const confirmInfo = ref<Customer>()
|
||||||
|
|
||||||
|
const tahesabRegisterVisible = ref(false)
|
||||||
|
const tahesabCustomer = ref<Customer>()
|
||||||
|
const tahesabEnabled = tahesabEnabledSetting
|
||||||
|
const pargarRegisterVisible = ref(false)
|
||||||
|
const pargarCustomer = ref<Customer>()
|
||||||
|
const pargarEnabled = pargarEnabledSetting
|
||||||
|
const customerDirectSmsEnabled = customerDirectSmsEnabledSetting
|
||||||
|
|
||||||
|
const accountingEnabled = computed(() => {
|
||||||
|
const provider = accountingProviderSetting.value
|
||||||
|
return provider === 'pargar' || provider === 'tahesab'
|
||||||
|
})
|
||||||
|
|
||||||
|
const editVisible = ref(false)
|
||||||
|
const editFormRef = ref<FormInstance>()
|
||||||
|
const editId = ref<number>()
|
||||||
|
const editForm = ref<Partial<Customer>>({})
|
||||||
|
|
||||||
|
function formatSeen(v?: string | null) {
|
||||||
|
return v ? toJalali(v) : '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSheet(row: Customer) {
|
||||||
|
selectedItem.value = row
|
||||||
|
sheetVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function goInfo(row: Customer) {
|
||||||
|
sheetVisible.value = false
|
||||||
|
void router.push({ name: 'customerMain_limitedOrder', query: { infoId: String(row.id) } })
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row: Customer) {
|
||||||
|
sheetVisible.value = false
|
||||||
|
editId.value = row.id
|
||||||
|
editForm.value = cloneDeep(row)
|
||||||
|
editVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitEdit() {
|
||||||
|
try {
|
||||||
|
await editFormRef.value?.validate()
|
||||||
|
if (!editId.value) return
|
||||||
|
actionLoading.value = true
|
||||||
|
await updateCustomer(editId.value, editForm.value as Customer)
|
||||||
|
message.success('ویرایش با موفقیت انجام شد')
|
||||||
|
editVisible.value = false
|
||||||
|
await load()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWalletCharge(row: Customer) {
|
||||||
|
sheetVisible.value = false
|
||||||
|
chargeCustomerId.value = row.id
|
||||||
|
showWalletCharge.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDirectSms(row: Customer) {
|
||||||
|
sheetVisible.value = false
|
||||||
|
directSmsCustomer.value = row
|
||||||
|
directSmsVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAccountingCodeEdit(row: Customer) {
|
||||||
|
sheetVisible.value = false
|
||||||
|
accountingCodeCustomer.value = row
|
||||||
|
accountingCodeVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openConfirmUser(row: Customer) {
|
||||||
|
sheetVisible.value = false
|
||||||
|
confirmInfo.value = row
|
||||||
|
confirmVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTahesabRegister(row: Customer) {
|
||||||
|
tahesabCustomer.value = row
|
||||||
|
tahesabRegisterVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPargarRegister(row: Customer) {
|
||||||
|
pargarCustomer.value = row
|
||||||
|
pargarRegisterVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArchive(row: Customer) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: 'آیا مشتری را بایگانی می کنید؟',
|
||||||
|
okText: 'تایید',
|
||||||
|
cancelText: 'رد',
|
||||||
|
centered: true,
|
||||||
|
zIndex: MODAL_ABOVE_ACTION_SHEET_Z_INDEX,
|
||||||
|
onOk: () => archiveFunction(row.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function archiveFunction(id?: number) {
|
||||||
|
if (!id) return
|
||||||
|
try {
|
||||||
|
actionLoading.value = true
|
||||||
|
await changeCustomerArchive(id)
|
||||||
|
message.success('مشتری با موفقیت بایگانی شد.')
|
||||||
|
sheetVisible.value = false
|
||||||
|
await load()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeCustomerActivationFunction(id?: number) {
|
||||||
|
if (!id) return
|
||||||
|
try {
|
||||||
|
actionLoading.value = true
|
||||||
|
await changeCustomerActivation(id)
|
||||||
|
message.success('تغییر وضعیت با موفقیت انجام شد')
|
||||||
|
await load()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeCustomerOrderActivationFunction(id?: number) {
|
||||||
|
if (!id) return
|
||||||
|
try {
|
||||||
|
actionLoading.value = true
|
||||||
|
await changeCustomerOrderActivation(id)
|
||||||
|
message.success('تغییر وضعیت سفارش دهی با موفقیت انجام شد')
|
||||||
|
await load()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmUserFunction() {
|
||||||
|
try {
|
||||||
|
actionLoading.value = true
|
||||||
|
await confirmUser(confirmInfo.value?.id)
|
||||||
|
confirmVisible.value = false
|
||||||
|
message.success('تایید کاربر انجام شد')
|
||||||
|
await load()
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getOnlineCustomers()
|
||||||
|
rows.value = res?.rows ?? []
|
||||||
|
count.value = res?.count ?? rows.value.length
|
||||||
|
if (selectedItem.value?.id) {
|
||||||
|
selectedItem.value = rows.value.find((r) => r.id === selectedItem.value?.id) ?? null
|
||||||
|
if (!selectedItem.value) sheetVisible.value = false
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
message.error('دریافت لیست آنلاین ممکن نیست')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void initAccountingSettingsFromApi()
|
||||||
|
void initWalletSettingsFromApi()
|
||||||
|
void load()
|
||||||
|
pollTimer = setInterval(() => {
|
||||||
|
void load()
|
||||||
|
}, 30000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (pollTimer) clearInterval(pollTimer)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.online-page__hero {
|
||||||
|
padding: 1rem 1.15rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid var(--admin-border, rgba(212, 175, 55, 0.35));
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(212, 175, 55, 0.12) 0%,
|
||||||
|
rgba(34, 197, 94, 0.08) 55%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-page__title {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--admin-text, #f5f5f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-page__sub {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--admin-text-secondary, #a3a3a3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-page__count {
|
||||||
|
@apply flex items-center gap-2 rounded-full px-3 py-1.5 text-sm;
|
||||||
|
background: rgba(34, 197, 94, 0.12);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.35);
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-page__count-dot {
|
||||||
|
@apply h-2.5 w-2.5 rounded-full bg-green-500 shrink-0;
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.55);
|
||||||
|
animation: online-pulse 1.6s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-page__empty {
|
||||||
|
@apply flex flex-col items-center justify-center py-16 text-center;
|
||||||
|
color: var(--admin-text-secondary, #a3a3a3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-page__empty-dot {
|
||||||
|
@apply h-3 w-3 rounded-full bg-gray-500 opacity-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-page__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(16.5rem, 1fr));
|
||||||
|
gap: 0.85rem;
|
||||||
|
max-height: calc(100vh - 14rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-card {
|
||||||
|
position: relative;
|
||||||
|
padding: 1rem 1rem 0.9rem;
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.28);
|
||||||
|
background: linear-gradient(160deg, rgba(34, 197, 94, 0.1) 0%, rgba(0, 0, 0, 0.2) 70%);
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-card:hover {
|
||||||
|
border-color: rgba(34, 197, 94, 0.55);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 3px;
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-card__top {
|
||||||
|
@apply flex items-center gap-2 mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-card__pulse {
|
||||||
|
@apply h-2.5 w-2.5 rounded-full bg-green-500;
|
||||||
|
animation: online-pulse 1.6s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-card__badge {
|
||||||
|
@apply text-xs font-700;
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-card__name {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--admin-text, #f5f5f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-card__mobile {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--admin-gold-light, #d4af37);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-card__meta {
|
||||||
|
@apply mt-3 flex items-center justify-between gap-2 text-xs;
|
||||||
|
color: var(--admin-text-secondary, #a3a3a3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-card__actions {
|
||||||
|
@apply mt-3 flex flex-wrap items-center justify-center gap-2 pt-2;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes online-pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.55);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 10px rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -18,14 +18,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import BasicMenu from '@/components/BasicMenu/index.vue'
|
import BasicMenu from '@/components/BasicMenu/index.vue'
|
||||||
import { useRouterStore } from '@core/stores/router'
|
import { useRouterStore } from '@core/stores/router'
|
||||||
import { get } from 'lodash'
|
import { get } from 'lodash'
|
||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
import { conditionalOrderEnabledSetting } from '@/utils/config'
|
import { conditionalOrderEnabledSetting, initWalletSettingsFromApi } from '@/utils/config'
|
||||||
|
|
||||||
const routerStore = useRouterStore()
|
const routerStore = useRouterStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void initWalletSettingsFromApi()
|
||||||
|
})
|
||||||
|
|
||||||
const menu = computed(() => {
|
const menu = computed(() => {
|
||||||
const item = routerStore.routes.find((item) => item.name === 'hesabKetab')
|
const item = routerStore.routes.find((item) => item.name === 'hesabKetab')
|
||||||
const children = get(item, 'children[0].children', []) as RouteRecordRaw[]
|
const children = get(item, 'children[0].children', []) as RouteRecordRaw[]
|
||||||
|
|||||||
@@ -800,10 +800,6 @@ defineExpose({
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-editor-tabs :deep(.ant-tabs-tab) {
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-editor-panel {
|
.product-editor-panel {
|
||||||
border: 1px solid var(--ant-color-border-secondary, rgba(0, 0, 0, 0.06));
|
border: 1px solid var(--ant-color-border-secondary, rgba(0, 0, 0, 0.06));
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ const Customers: RouteRecordRaw = {
|
|||||||
meta: {
|
meta: {
|
||||||
// permissionsKey: 'config.limitedOrder',
|
// permissionsKey: 'config.limitedOrder',
|
||||||
icon: 'vuesax-linear:people',
|
icon: 'vuesax-linear:people',
|
||||||
title: 'لیست مشتریان'
|
title: 'لیست مشتریان',
|
||||||
|
order: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -45,7 +46,18 @@ const Customers: RouteRecordRaw = {
|
|||||||
meta: {
|
meta: {
|
||||||
// permissionsKey: 'user',
|
// permissionsKey: 'user',
|
||||||
icon: 'vuesax-linear:save-2',
|
icon: 'vuesax-linear:save-2',
|
||||||
title: 'بایگانی'
|
title: 'بایگانی',
|
||||||
|
order: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customerMain_online',
|
||||||
|
path: 'online',
|
||||||
|
component: () => import('@/pages/menuPages/customers/online/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'vuesax-linear:wifi',
|
||||||
|
title: 'کاربران آنلاین',
|
||||||
|
order: 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -56,7 +68,7 @@ const Customers: RouteRecordRaw = {
|
|||||||
// permissionsKey: 'user',
|
// permissionsKey: 'user',
|
||||||
icon: 'vuesax-linear:card-tick',
|
icon: 'vuesax-linear:card-tick',
|
||||||
title: 'احراز هویت',
|
title: 'احراز هویت',
|
||||||
|
order: 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-11
@@ -163,18 +163,9 @@ async function bootstrapSettingsFromApi(): Promise<void> {
|
|||||||
setCachedMainMetal(mainMetalRow?.value)
|
setCachedMainMetal(mainMetalRow?.value)
|
||||||
const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet')
|
const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet')
|
||||||
setCachedMainWalletProductIds(parseMainWalletProductIds(mainWalletRow?.value))
|
setCachedMainWalletProductIds(parseMainWalletProductIds(mainWalletRow?.value))
|
||||||
} catch {
|
|
||||||
storeModeFromSettings = false
|
|
||||||
setBannerEnabledFromSettings(false)
|
|
||||||
setTrackingSystemsEnabledFromSettings(false)
|
|
||||||
setCustomerDirectSmsEnabledFromSettings(false)
|
|
||||||
setConditionalOrderEnabledFromSettings(false)
|
|
||||||
applyAccountingProviderFromRows([])
|
|
||||||
setCachedMainMetal(null)
|
|
||||||
const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet')
|
|
||||||
setCachedMainWalletProductIds([])
|
|
||||||
} finally {
|
|
||||||
settingsBootstrapped = true
|
settingsBootstrapped = true
|
||||||
|
} catch {
|
||||||
|
// Allow retry on next init* call — do not lock flags to false forever.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/** همتراز با CustomerPresenceService::ONLINE_THRESHOLD_SECONDS روی سرور */
|
||||||
|
export const CUSTOMER_ONLINE_THRESHOLD_MS = 2 * 60 * 1000
|
||||||
|
|
||||||
|
export type CustomerPresenceKind = 'online' | 'offline' | 'never'
|
||||||
|
|
||||||
|
export function getCustomerPresenceKind(
|
||||||
|
lastSeenAt: string | Date | null | undefined
|
||||||
|
): CustomerPresenceKind {
|
||||||
|
if (!lastSeenAt) return 'never'
|
||||||
|
const t = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime()
|
||||||
|
if (!Number.isFinite(t)) return 'never'
|
||||||
|
if (Date.now() - t < CUSTOMER_ONLINE_THRESHOLD_MS) return 'online'
|
||||||
|
return 'offline'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCustomerOnline(lastSeenAt: string | Date | null | undefined): boolean {
|
||||||
|
return getCustomerPresenceKind(lastSeenAt) === 'online'
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user