added online and trigger
This commit is contained in:
@@ -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">
|
||||
<template #default="{ item: row }">
|
||||
<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>
|
||||
|
||||
<div class="customer-mobile-sheet__chips">
|
||||
@@ -100,7 +101,6 @@
|
||||
/>
|
||||
<MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" />
|
||||
<MobileDetailRow label="نام معرف" :value="row.referral?.name ?? '-'" />
|
||||
<MobileDetailRow label="آخرین بازدید" :value="lastSeen(row)" value-class="text-xs" />
|
||||
</MobileInfoGrid>
|
||||
</div>
|
||||
</template>
|
||||
@@ -118,13 +118,13 @@ import MobileDetailRow from '@/components/mobile/MobileDetailRow.vue'
|
||||
import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue'
|
||||
import MobileInfoGrid from '@/components/mobile/MobileInfoGrid.vue'
|
||||
import CustomerWalletBalancePanel from '@/components/customer/CustomerWalletBalancePanel.vue'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
import {
|
||||
accountingProviderSetting,
|
||||
customerDirectSmsEnabledSetting,
|
||||
initAccountingSettingsFromApi
|
||||
} from '@/utils/config'
|
||||
import { displayAccountingCode } from '@/utils/accountingCode'
|
||||
import CustomerPresenceBadge from '@/pages/menuPages/customers/base/components/CustomerPresenceBadge.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
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() {
|
||||
try {
|
||||
const res = await getChargeTargets()
|
||||
@@ -260,6 +256,16 @@ watch(
|
||||
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 {
|
||||
width: 100%;
|
||||
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 { toJalali } from '@core/utils/useDataTime'
|
||||
import CustomTag from '@/components/CustomTag.vue'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
import { getCashBalance } from '@/components/mobile/utils/wallet'
|
||||
import { customerWalletMetalBalanceTitle } from '@/utils/adminMetalLabels'
|
||||
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
|
||||
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 }
|
||||
|
||||
@@ -63,24 +63,12 @@ export function buildCustomerColumns(
|
||||
key: 'mobile',
|
||||
type: 'Number'
|
||||
},
|
||||
{
|
||||
title: 'نوع کاربر',
|
||||
dataIndex: 'type_str',
|
||||
key: 'type_str',
|
||||
type: 'Text'
|
||||
},
|
||||
{
|
||||
title: 'کد معرف',
|
||||
dataIndex: 'referral_id',
|
||||
key: 'referral_id',
|
||||
type: 'Text'
|
||||
},
|
||||
{
|
||||
title: 'نام معرف کاربر',
|
||||
dataIndex: 'referral.name',
|
||||
key: 'referral.name',
|
||||
type: 'Text'
|
||||
},
|
||||
{
|
||||
title: 'موجودی ریالی (ریال)',
|
||||
dataIndex: 'wallet.cash',
|
||||
@@ -122,53 +110,42 @@ export function buildCustomerColumns(
|
||||
return [
|
||||
...baseColumns,
|
||||
...metalColumns,
|
||||
/* {
|
||||
title: 'اخرین بازدید',
|
||||
{
|
||||
title: 'آخرین بازدید',
|
||||
dataIndex: 'last_seen_at',
|
||||
key: 'last_seen_at',
|
||||
type: 'DateTime',
|
||||
customRender(data) {
|
||||
return toJalali(data.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>
|
||||
)
|
||||
}
|
||||
width: 130,
|
||||
filterable: false,
|
||||
sortable: true,
|
||||
customRender: ({ record }) => <CustomerPresenceCell lastSeenAt={record.last_seen_at} />
|
||||
},
|
||||
{
|
||||
title: 'وضعیت سفارش دهی',
|
||||
dataIndex: 'trade_active',
|
||||
key: 'trade_active',
|
||||
sortable: false,
|
||||
title: 'وضعیت',
|
||||
dataIndex: 'active',
|
||||
key: 'status',
|
||||
type: 'Boolean',
|
||||
width: 132,
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
customRender: ({ record }) => {
|
||||
return record.trade_active ? (
|
||||
<div class="flex justify-center">
|
||||
<CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
|
||||
مجاز
|
||||
const userOk = Boolean(record.active)
|
||||
const tradeOk = Boolean(record.trade_active)
|
||||
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>
|
||||
</div>
|
||||
) : (
|
||||
<div class="flex justify-center">
|
||||
<CustomTag textColor="#C53030" bgColor="#FECACA" class={'font-bold px-6'}>
|
||||
غیر مجاز
|
||||
<CustomTag
|
||||
textColor={tradeOk ? '#25855A' : '#C53030'}
|
||||
bgColor={tradeOk ? '#E3F4E9' : '#FECACA'}
|
||||
class="m-0 font-bold !px-2.5 !py-0 text-xs leading-6 whitespace-nowrap"
|
||||
>
|
||||
{tradeOk ? 'سفارش مجاز' : 'سفارش غیرمجاز'}
|
||||
</CustomTag>
|
||||
</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>
|
||||
<div class="font-bold">{{ userInfo.name }}</div>
|
||||
<CustomerPresenceBadge :last-seen-at="userInfo?.last_seen_at" />
|
||||
<div class="mt-1">{{ userInfo?.mobile }}</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-center pl-3">
|
||||
@@ -178,7 +179,6 @@
|
||||
title="حداکثر معرفی"
|
||||
: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">
|
||||
<CustomTag
|
||||
:textColor="userInfo?.trade_active === 1 ? '#25855A' : '#C53030'"
|
||||
@@ -188,41 +188,6 @@
|
||||
{{ userInfo?.trade_active === 1 ? 'مجاز' : 'غیر مجاز' }}
|
||||
</CustomTag>
|
||||
</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 v-if="userInfo?.id && isMobile" class="mt-2 space-y-2">
|
||||
@@ -278,6 +243,19 @@
|
||||
<a-tab-pane key="قلک ها" tab="قلک ها">
|
||||
<FundsTable ref="fundTable" :customerId="userInfo?.id" />
|
||||
</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>
|
||||
</BasicActionSheet>
|
||||
<WalletChargeModal
|
||||
@@ -314,15 +292,17 @@ import { confirmDeleteItem } from '@/utils/confirmModal'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import type { Item } from '@/model/Item'
|
||||
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 { usePrice } from '@core/utils/usePrice'
|
||||
import jalaliday from 'jalaliday'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ordersTbl from '@/pages/menuPages/orders/components/table.vue'
|
||||
import transactionsTbl from '@/pages/menuPages/transactions/components/table.vue'
|
||||
import smsTbl from '@/pages/menuPages/sms/components/table.vue'
|
||||
import FundsTable from '@/pages/menuPages/customers/base/components/fundsTable.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 Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
@@ -340,10 +320,9 @@ import {
|
||||
initWalletSettingsFromApi
|
||||
} from '@/utils/config'
|
||||
import { displayAccountingCode, isValidAccountingCode } from '@/utils/accountingCode'
|
||||
import CustomerPresenceBadge from '@/pages/menuPages/customers/base/components/CustomerPresenceBadge.vue'
|
||||
import { syncCustomerWalletFromPargar, type PargarSyncWalletResult } from '@/api/pargar'
|
||||
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 = {
|
||||
description: 'نام کوچک مشتری که در اپ و پنل نمایش داده میشود.',
|
||||
@@ -576,12 +555,42 @@ const resetData = () => {
|
||||
const deleteItem = (id: number) => confirmDeleteItem('کاربر', id, deleteCustomer, refreshTable)
|
||||
|
||||
//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 () => {
|
||||
loading.value = true
|
||||
await Promise.allSettled([initAccountingSettingsFromApi(), initWalletSettingsFromApi()])
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.query.infoId,
|
||||
() => {
|
||||
void openInfoFromQuery()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
//add project modal
|
||||
const addUserVisible = ref<boolean>(false)
|
||||
const userInfoVisible = ref<boolean>(false)
|
||||
@@ -603,57 +612,20 @@ const accountingCodeVisible = computed(() => {
|
||||
return provider === 'pargar' || provider === 'tahesab'
|
||||
})
|
||||
|
||||
const conditionalOrderEnabled = conditionalOrderEnabledSetting
|
||||
const conditionalOrderLoading = ref(false)
|
||||
const conditionalOrderEnabled = computed(() => conditionalOrderEnabledSetting.value)
|
||||
|
||||
async function toggleConditionalOrderActive() {
|
||||
if (!userInfo.value?.id || conditionalOrderLoading.value) return
|
||||
conditionalOrderLoading.value = true
|
||||
try {
|
||||
const res = await changeCustomerConditionalOrderActive(userInfo.value.id)
|
||||
userInfo.value.conditional_order_active = Boolean(
|
||||
res?.conditional_order_active ?? !userInfo.value.conditional_order_active
|
||||
)
|
||||
refreshTable()
|
||||
message.success('مجوز سفارش شرطی بهروز شد')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
conditionalOrderLoading.value = false
|
||||
function onConditionalOrderUpdated(payload: {
|
||||
conditional_order_active?: boolean
|
||||
conditional_order_limit?: number | null
|
||||
}) {
|
||||
if (!userInfo.value) return
|
||||
if (payload.conditional_order_active !== undefined) {
|
||||
userInfo.value.conditional_order_active = payload.conditional_order_active
|
||||
}
|
||||
}
|
||||
|
||||
function editConditionalOrderLimit() {
|
||||
if (!userInfo.value?.id) return
|
||||
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('سقف سفارش شرطی ذخیره شد')
|
||||
},
|
||||
})
|
||||
if (payload.conditional_order_limit !== undefined) {
|
||||
userInfo.value.conditional_order_limit = payload.conditional_order_limit
|
||||
}
|
||||
refreshTable()
|
||||
}
|
||||
|
||||
const accountingCodeValue = computed(() => {
|
||||
|
||||
Reference in New Issue
Block a user