all customer fixing added
This commit is contained in:
@@ -56,6 +56,32 @@ export const syncCustomerWalletFromPargar = async (customerId: number, force = f
|
|||||||
{ force }
|
{ force }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export interface PargarBulkWalletSyncChunk {
|
||||||
|
ok: boolean
|
||||||
|
message: string
|
||||||
|
total: number
|
||||||
|
offset: number
|
||||||
|
processed: number
|
||||||
|
next_offset: number
|
||||||
|
done: boolean
|
||||||
|
success: number
|
||||||
|
changed: number
|
||||||
|
failed: number
|
||||||
|
errors: { id: number; name: string; mobile?: string; error: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** یک دسته از همگامسازی ماندهٔ همهٔ مشتریان دارای کد پرگار */
|
||||||
|
export const syncAllWalletsFromPargarChunk = async (params?: {
|
||||||
|
offset?: number
|
||||||
|
limit?: number
|
||||||
|
force?: boolean
|
||||||
|
}) =>
|
||||||
|
await useAxios.post<PargarBulkWalletSyncChunk>('/admin/pargar/wallet-sync/all', {
|
||||||
|
offset: params?.offset ?? 0,
|
||||||
|
limit: params?.limit ?? 20,
|
||||||
|
force: params?.force ?? false,
|
||||||
|
})
|
||||||
|
|
||||||
export interface PanelOnlyCustomer {
|
export interface PanelOnlyCustomer {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="cs-wrap">
|
<div class="cs-wrap">
|
||||||
|
|
||||||
|
<PargarWalletBulkSync />
|
||||||
|
|
||||||
<!-- ─── بنر آمار ─── -->
|
<!-- ─── بنر آمار ─── -->
|
||||||
<div class="cs-banner">
|
<div class="cs-banner">
|
||||||
<div class="cs-banner__title">همگامسازی مشتریان</div>
|
<div class="cs-banner__title">همگامسازی مشتریان</div>
|
||||||
@@ -323,6 +325,7 @@ import {
|
|||||||
type SyncPanelResult,
|
type SyncPanelResult,
|
||||||
} from '@/api/pargar'
|
} from '@/api/pargar'
|
||||||
import { useCustomerImportStore } from '@/store/customerImportStore'
|
import { useCustomerImportStore } from '@/store/customerImportStore'
|
||||||
|
import PargarWalletBulkSync from './PargarWalletBulkSync.vue'
|
||||||
|
|
||||||
const importStore = useCustomerImportStore()
|
const importStore = useCustomerImportStore()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pw-bulk">
|
||||||
|
<div class="pw-bulk__head">
|
||||||
|
<div class="pw-bulk__icon" aria-hidden="true">
|
||||||
|
<Icon icon="vuesax-linear:refresh-circle" :size="20" color="var(--admin-primary, #10913A)" />
|
||||||
|
</div>
|
||||||
|
<div class="pw-bulk__copy min-w-0 flex-1">
|
||||||
|
<p class="pw-bulk__title">همگامسازی ماندهٔ همهٔ مشتریان</p>
|
||||||
|
<p class="pw-bulk__hint">
|
||||||
|
موجودی کیف همهٔ مشتریان دارای کد پرگار را با ماندهٔ پرگار یکی میکند — تاریخچهٔ اسناد روی کیف
|
||||||
|
replay نمیشود
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
class="pw-bulk__btn"
|
||||||
|
:loading="running"
|
||||||
|
:disabled="running"
|
||||||
|
@click="confirmAndStart"
|
||||||
|
>
|
||||||
|
همگامسازی مانده از پرگار برای همه
|
||||||
|
</a-button>
|
||||||
|
|
||||||
|
<div v-if="running || resultVisible" class="pw-bulk__progress">
|
||||||
|
<template v-if="running">
|
||||||
|
<div class="pw-bulk__progress-row">
|
||||||
|
<a-spin size="small" />
|
||||||
|
<span>
|
||||||
|
در حال همگامسازی…
|
||||||
|
<template v-if="total > 0">{{ processed }} / {{ total }}</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="pw-bulk__track">
|
||||||
|
<div class="pw-bulk__track-fill" :style="{ width: percent + '%' }" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="resultVisible">
|
||||||
|
<div class="pw-bulk__progress-row">
|
||||||
|
<span class="pw-bulk__ok">✓ {{ success }} موفق</span>
|
||||||
|
<span v-if="changed" class="pw-bulk__changed">{{ changed }} تغییر موجودی</span>
|
||||||
|
<span v-if="failed" class="pw-bulk__fail">✗ {{ failed }} ناموفق</span>
|
||||||
|
<a-button v-if="errors.length" type="link" size="small" @click="showErrors = !showErrors">
|
||||||
|
{{ showErrors ? 'پنهان' : 'جزئیات' }}
|
||||||
|
</a-button>
|
||||||
|
<a-button type="link" size="small" @click="dismissResult">بستن</a-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="showErrors && errors.length" class="pw-bulk__errors">
|
||||||
|
<div v-for="e in errors" :key="e.id">
|
||||||
|
<b>{{ e.name }}</b>
|
||||||
|
<template v-if="e.mobile"> ({{ e.mobile }})</template>
|
||||||
|
— {{ e.error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, createVNode, ref } from 'vue'
|
||||||
|
import { Modal, message } from 'ant-design-vue'
|
||||||
|
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||||
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
|
import {
|
||||||
|
syncAllWalletsFromPargarChunk,
|
||||||
|
type PargarBulkWalletSyncChunk,
|
||||||
|
} from '@/api/pargar'
|
||||||
|
|
||||||
|
const CHUNK_LIMIT = 20
|
||||||
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||||
|
|
||||||
|
const running = ref(false)
|
||||||
|
const resultVisible = ref(false)
|
||||||
|
const showErrors = ref(false)
|
||||||
|
|
||||||
|
const total = ref(0)
|
||||||
|
const processed = ref(0)
|
||||||
|
const success = ref(0)
|
||||||
|
const changed = ref(0)
|
||||||
|
const failed = ref(0)
|
||||||
|
const errors = ref<PargarBulkWalletSyncChunk['errors']>([])
|
||||||
|
|
||||||
|
const percent = computed(() =>
|
||||||
|
total.value > 0 ? Math.min(100, Math.round((processed.value / total.value) * 100)) : 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
function dismissResult() {
|
||||||
|
resultVisible.value = false
|
||||||
|
showErrors.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmAndStart() {
|
||||||
|
if (running.value) return
|
||||||
|
Modal.confirm({
|
||||||
|
title: 'همگامسازی ماندهٔ همهٔ مشتریان؟',
|
||||||
|
icon: createVNode(ExclamationCircleOutlined),
|
||||||
|
content:
|
||||||
|
'ماندهٔ کیف همهٔ مشتریان دارای کد پرگار با پرگار همتراز میشود. این کار ممکن است چند دقیقه طول بکشد.',
|
||||||
|
okText: 'شروع همگامسازی',
|
||||||
|
cancelText: 'انصراف',
|
||||||
|
centered: true,
|
||||||
|
onOk: () => start(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
running.value = true
|
||||||
|
resultVisible.value = false
|
||||||
|
showErrors.value = false
|
||||||
|
total.value = 0
|
||||||
|
processed.value = 0
|
||||||
|
success.value = 0
|
||||||
|
changed.value = 0
|
||||||
|
failed.value = 0
|
||||||
|
errors.value = []
|
||||||
|
|
||||||
|
let offset = 0
|
||||||
|
let done = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (!done) {
|
||||||
|
const res = await syncAllWalletsFromPargarChunk({
|
||||||
|
offset,
|
||||||
|
limit: CHUNK_LIMIT,
|
||||||
|
force: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
total.value = res.total
|
||||||
|
success.value += res.success
|
||||||
|
changed.value += res.changed
|
||||||
|
failed.value += res.failed
|
||||||
|
if (res.errors?.length) {
|
||||||
|
errors.value = [...errors.value, ...res.errors]
|
||||||
|
}
|
||||||
|
|
||||||
|
processed.value = Math.min(res.next_offset, res.total)
|
||||||
|
offset = res.next_offset
|
||||||
|
done = res.done || res.processed === 0 || res.total === 0
|
||||||
|
|
||||||
|
if (!done) await sleep(150)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultVisible.value = true
|
||||||
|
if (failed.value === 0) {
|
||||||
|
message.success(
|
||||||
|
total.value === 0
|
||||||
|
? 'مشتری دارای کد پرگار یافت نشد'
|
||||||
|
: `همگامسازی ${success.value} مشتری انجام شد`,
|
||||||
|
)
|
||||||
|
} else if (success.value > 0) {
|
||||||
|
message.warning(`${success.value} موفق — ${failed.value} ناموفق`)
|
||||||
|
} else {
|
||||||
|
message.error('همگامسازی ناموفق بود')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
message.error('خطا در ارتباط با سرور')
|
||||||
|
resultVisible.value = processed.value > 0 || success.value > 0 || failed.value > 0
|
||||||
|
} finally {
|
||||||
|
running.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pw-bulk {
|
||||||
|
@apply rounded-xl border border-base p-3;
|
||||||
|
background: var(--admin-surface-muted, var(--admin-bg-muted, #f8fafc));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-bulk__head {
|
||||||
|
@apply mb-3 flex items-start gap-2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-bulk__icon {
|
||||||
|
@apply flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gray-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-bulk__title {
|
||||||
|
@apply m-0 text-sm font-700;
|
||||||
|
color: var(--admin-text, inherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-bulk__hint {
|
||||||
|
@apply m-0 mt-0.5 text-xs leading-5;
|
||||||
|
color: var(--admin-text-secondary, #6b7280);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-bulk__btn {
|
||||||
|
@apply font-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-bulk__progress {
|
||||||
|
@apply mt-3 rounded-lg border border-base p-2.5 text-xs;
|
||||||
|
background: var(--admin-bg-surface, #fff);
|
||||||
|
color: var(--admin-text, inherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-bulk__progress-row {
|
||||||
|
@apply flex flex-wrap items-center gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-bulk__track {
|
||||||
|
@apply mt-2 h-1.5 overflow-hidden rounded-full;
|
||||||
|
background: var(--admin-border, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-bulk__track-fill {
|
||||||
|
@apply h-full rounded-full transition-all duration-300;
|
||||||
|
background: var(--admin-primary, #10913A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-bulk__ok {
|
||||||
|
@apply rounded-full px-2.5 py-0.5 font-600;
|
||||||
|
background: rgba(52, 211, 153, 0.15);
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-bulk__changed {
|
||||||
|
@apply rounded-full px-2.5 py-0.5 font-600;
|
||||||
|
background: rgba(14, 165, 233, 0.12);
|
||||||
|
color: #0284c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-bulk__fail {
|
||||||
|
@apply rounded-full px-2.5 py-0.5 font-600;
|
||||||
|
background: rgba(239, 68, 68, 0.12);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-bulk__errors {
|
||||||
|
@apply mt-2 max-h-28 overflow-y-auto rounded-lg p-2 text-xs leading-6;
|
||||||
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -174,7 +174,7 @@ import SettingValueField from './SettingValueField.vue'
|
|||||||
import SettingCard from './SettingCard.vue'
|
import SettingCard from './SettingCard.vue'
|
||||||
import { refreshAppModeMenu } from '@/router'
|
import { refreshAppModeMenu } from '@/router'
|
||||||
import { useUserStore } from '@/store/userStore'
|
import { useUserStore } from '@/store/userStore'
|
||||||
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, setBannerEnabledFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings } from '@/utils/config'
|
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings } from '@/utils/config'
|
||||||
import {
|
import {
|
||||||
EDITABLE_SETTING_GROUPS,
|
EDITABLE_SETTING_GROUPS,
|
||||||
SYSTEM_SETTING_GROUPS,
|
SYSTEM_SETTING_GROUPS,
|
||||||
@@ -279,7 +279,8 @@ const filteredRows = computed(() =>
|
|||||||
(baseData.value || []).filter((row) => {
|
(baseData.value || []).filter((row) => {
|
||||||
if (row.id == null || !matchesFilter(row)) return false
|
if (row.id == null || !matchesFilter(row)) return false
|
||||||
if (isSystemMode.value) {
|
if (isSystemMode.value) {
|
||||||
// API با system_only=1 فیلتر کرده؛ فقط جستجو اعمال میشود
|
// کارمزد سفارش شرطی فقط وقتی قابلیت سراسری روشن است دیده شود
|
||||||
|
if (row.key === 'fee_conditional_order' && !isSettingVisible(row.key)) return false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return isEditable(row) && isSettingVisible(row.key)
|
return isEditable(row) && isSettingVisible(row.key)
|
||||||
@@ -425,9 +426,11 @@ async function initConfig() {
|
|||||||
if (isSystemMode.value) {
|
if (isSystemMode.value) {
|
||||||
applyBannerEnabledFromRows(baseData.value)
|
applyBannerEnabledFromRows(baseData.value)
|
||||||
applyTrackingSystemsEnabledFromRows(baseData.value)
|
applyTrackingSystemsEnabledFromRows(baseData.value)
|
||||||
|
applyConditionalOrderEnabledFromRows(baseData.value)
|
||||||
} else {
|
} else {
|
||||||
applyBannerEnabledFromRows(baseData.value)
|
applyBannerEnabledFromRows(baseData.value)
|
||||||
applyTrackingSystemsEnabledFromRows(baseData.value)
|
applyTrackingSystemsEnabledFromRows(baseData.value)
|
||||||
|
applyConditionalOrderEnabledFromRows(baseData.value)
|
||||||
const storeRow = baseData.value.find((row) => row.key === 'is_store')
|
const storeRow = baseData.value.find((row) => row.key === 'is_store')
|
||||||
if (storeRow) {
|
if (storeRow) {
|
||||||
setStoreModeFromSettings(isSwitchOn(storeRow.value))
|
setStoreModeFromSettings(isSwitchOn(storeRow.value))
|
||||||
@@ -544,6 +547,9 @@ async function saveSettingRow(row: Const) {
|
|||||||
if (row.key === 'tracking_systems_enabled') {
|
if (row.key === 'tracking_systems_enabled') {
|
||||||
setTrackingSystemsEnabledFromSettings(isSwitchOn(value))
|
setTrackingSystemsEnabledFromSettings(isSwitchOn(value))
|
||||||
}
|
}
|
||||||
|
if (row.key === 'enable_conditional_order') {
|
||||||
|
setConditionalOrderEnabledFromSettings(isSwitchOn(value))
|
||||||
|
}
|
||||||
savedFlashIds.value.add(id)
|
savedFlashIds.value.add(id)
|
||||||
if (saveTimers.has(id)) clearTimeout(saveTimers.get(id))
|
if (saveTimers.has(id)) clearTimeout(saveTimers.get(id))
|
||||||
saveTimers.set(
|
saveTimers.set(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Const } from '@/model/configuration'
|
import type { Const } from '@/model/configuration'
|
||||||
import { isStoreApp } from '@/utils/config'
|
import { conditionalOrderEnabledSetting, isStoreApp } from '@/utils/config'
|
||||||
|
|
||||||
export type SettingFieldType =
|
export type SettingFieldType =
|
||||||
| 'text'
|
| 'text'
|
||||||
@@ -208,6 +208,7 @@ export function isSettingVisible(key: string | undefined | null): boolean {
|
|||||||
if (!k) return false
|
if (!k) return false
|
||||||
if (HIDDEN_LEGACY_SETTING_KEYS.has(k)) return false
|
if (HIDDEN_LEGACY_SETTING_KEYS.has(k)) return false
|
||||||
if (STORE_ONLY_SETTING_KEYS.has(k) && !isStoreApp()) return false
|
if (STORE_ONLY_SETTING_KEYS.has(k) && !isStoreApp()) return false
|
||||||
|
if (k === 'fee_conditional_order' && !conditionalOrderEnabledSetting.value) return false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,47 +113,6 @@
|
|||||||
:color="record.trade_active === 1 ? '#54AD06' : '#ad0627'"
|
:color="record.trade_active === 1 ? '#54AD06' : '#ad0627'"
|
||||||
/>
|
/>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip
|
|
||||||
:key="`co-${record.conditional_order_active}-${record.id}`"
|
|
||||||
placement="bottom"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="changeConditionalOrderActiveFunction(record.id)"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<span>
|
|
||||||
{{
|
|
||||||
record.conditional_order_active
|
|
||||||
? 'محدود کردن سفارش شرطی'
|
|
||||||
: 'مجاز کردن سفارش شرطی'
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<Icon
|
|
||||||
:icon="
|
|
||||||
record.conditional_order_active
|
|
||||||
? 'vuesax-linear:timer'
|
|
||||||
: 'vuesax-linear:timer-pause'
|
|
||||||
"
|
|
||||||
:color="record.conditional_order_active ? '#54AD06' : '#ad0627'"
|
|
||||||
/>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip
|
|
||||||
placement="bottom"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="openConditionalOrderLimit(record)"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<span>
|
|
||||||
سقف سفارش شرطی
|
|
||||||
{{
|
|
||||||
record.conditional_order_limit
|
|
||||||
? `(${record.conditional_order_limit})`
|
|
||||||
: '(نامحدود)'
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<Icon icon="vuesax-linear:slider-horizontal" color="#4572E9" />
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
v-if="customerDirectSmsEnabled"
|
v-if="customerDirectSmsEnabled"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
@@ -252,8 +211,6 @@ import {
|
|||||||
changeCustomerActivation,
|
changeCustomerActivation,
|
||||||
changeCustomerArchive,
|
changeCustomerArchive,
|
||||||
changeCustomerOrderActivation,
|
changeCustomerOrderActivation,
|
||||||
changeCustomerConditionalOrderActive,
|
|
||||||
updateCustomerConditionalOrderLimit,
|
|
||||||
confirmUser,
|
confirmUser,
|
||||||
getCustomerTable
|
getCustomerTable
|
||||||
} from '@/api/customer'
|
} from '@/api/customer'
|
||||||
@@ -427,49 +384,6 @@ async function changeCustomerOrderActivationFunction(id: number) {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeConditionalOrderActiveFunction(id: number) {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
await changeCustomerConditionalOrderActive(id)
|
|
||||||
await fetch()
|
|
||||||
message.success('مجوز سفارش شرطی بهروز شد')
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openConditionalOrderLimit(customer: Customer) {
|
|
||||||
const current = customer.conditional_order_limit
|
|
||||||
Modal.confirm({
|
|
||||||
title: 'سقف تعداد سفارش شرطی',
|
|
||||||
content: `برای «${customer.name || customer.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()
|
|
||||||
}
|
|
||||||
await updateCustomerConditionalOrderLimit(customer.id, limit)
|
|
||||||
await fetch()
|
|
||||||
message.success('سقف سفارش شرطی ذخیره شد')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
|||||||
@@ -188,6 +188,41 @@
|
|||||||
{{ 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">
|
||||||
@@ -279,7 +314,7 @@ 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 } from '@/api/customer'
|
import { createCustomer, deleteCustomer, updateCustomer, changeCustomerConditionalOrderActive, updateCustomerConditionalOrderLimit } 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'
|
||||||
@@ -300,11 +335,15 @@ import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
|
|||||||
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
|
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
|
||||||
import {
|
import {
|
||||||
accountingProviderSetting,
|
accountingProviderSetting,
|
||||||
initAccountingSettingsFromApi
|
conditionalOrderEnabledSetting,
|
||||||
|
initAccountingSettingsFromApi,
|
||||||
|
initWalletSettingsFromApi
|
||||||
} from '@/utils/config'
|
} from '@/utils/config'
|
||||||
import { displayAccountingCode, isValidAccountingCode } from '@/utils/accountingCode'
|
import { displayAccountingCode, isValidAccountingCode } from '@/utils/accountingCode'
|
||||||
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: 'نام کوچک مشتری که در اپ و پنل نمایش داده میشود.',
|
||||||
@@ -539,7 +578,7 @@ const deleteItem = (id: number) => confirmDeleteItem('کاربر', id, deleteCus
|
|||||||
//define selects
|
//define selects
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await initAccountingSettingsFromApi()
|
await Promise.allSettled([initAccountingSettingsFromApi(), initWalletSettingsFromApi()])
|
||||||
loading.value = false
|
loading.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -564,6 +603,59 @@ const accountingCodeVisible = computed(() => {
|
|||||||
return provider === 'pargar' || provider === 'tahesab'
|
return provider === 'pargar' || provider === 'tahesab'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const conditionalOrderEnabled = conditionalOrderEnabledSetting
|
||||||
|
const conditionalOrderLoading = ref(false)
|
||||||
|
|
||||||
|
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 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('سقف سفارش شرطی ذخیره شد')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const accountingCodeValue = computed(() => {
|
const accountingCodeValue = computed(() => {
|
||||||
const provider = accountingProviderSetting.value
|
const provider = accountingProviderSetting.value
|
||||||
if (provider === 'pargar') return displayAccountingCode(userInfo.value?.pargar_account_id)
|
if (provider === 'pargar') return displayAccountingCode(userInfo.value?.pargar_account_id)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ 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'
|
||||||
|
|
||||||
const routerStore = useRouterStore()
|
const routerStore = useRouterStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -30,6 +31,7 @@ const menu = computed(() => {
|
|||||||
const children = get(item, 'children[0].children', []) as RouteRecordRaw[]
|
const children = get(item, 'children[0].children', []) as RouteRecordRaw[]
|
||||||
return children
|
return children
|
||||||
.filter((child) => !child.meta?.hideMenu)
|
.filter((child) => !child.meta?.hideMenu)
|
||||||
|
.filter((child) => child.name !== 'conditionalOrders' || conditionalOrderEnabledSetting.value)
|
||||||
.sort((a, b) => (Number(a.meta?.order) || 99) - (Number(b.meta?.order) || 99))
|
.sort((a, b) => (Number(a.meta?.order) || 99) - (Number(b.meta?.order) || 99))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
import { conditionalOrderEnabledSetting } from '@/utils/config'
|
||||||
|
|
||||||
const HesabKetab: RouteRecordRaw = {
|
const HesabKetab: RouteRecordRaw = {
|
||||||
name: 'hesabKetab',
|
name: 'hesabKetab',
|
||||||
@@ -98,6 +99,11 @@ const HesabKetab: RouteRecordRaw = {
|
|||||||
icon: 'vuesax-linear:timer',
|
icon: 'vuesax-linear:timer',
|
||||||
title: 'سفارش شرطی',
|
title: 'سفارش شرطی',
|
||||||
order: 6.5
|
order: 6.5
|
||||||
|
},
|
||||||
|
beforeEnter() {
|
||||||
|
if (!conditionalOrderEnabledSetting.value) {
|
||||||
|
return { name: 'accountsMain' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ export const pargarEnabledSetting = ref(false)
|
|||||||
/** reactive — دکمه پیامک تکی در لیست مشتریان */
|
/** reactive — دکمه پیامک تکی در لیست مشتریان */
|
||||||
export const customerDirectSmsEnabledSetting = ref(false)
|
export const customerDirectSmsEnabledSetting = ref(false)
|
||||||
|
|
||||||
|
/** reactive — منو و UI سفارش شرطی (ادمین + کنترلهای مشتری) */
|
||||||
|
export const conditionalOrderEnabledSetting = ref(false)
|
||||||
|
|
||||||
export const accountingProviderSetting = ref<AccountingProvider>('none')
|
export const accountingProviderSetting = ref<AccountingProvider>('none')
|
||||||
|
|
||||||
let tahesabEnabledFromSettings: boolean | null = null
|
let tahesabEnabledFromSettings: boolean | null = null
|
||||||
@@ -112,6 +115,10 @@ export function setCustomerDirectSmsEnabledFromSettings(value: boolean): void {
|
|||||||
customerDirectSmsEnabledSetting.value = value
|
customerDirectSmsEnabledSetting.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setConditionalOrderEnabledFromSettings(value: boolean): void {
|
||||||
|
conditionalOrderEnabledSetting.value = value
|
||||||
|
}
|
||||||
|
|
||||||
export function applyBannerEnabledFromRows(rows: Array<{ key?: string; value?: string | null }>) {
|
export function applyBannerEnabledFromRows(rows: Array<{ key?: string; value?: string | null }>) {
|
||||||
const row = rows.find((item) => item.key === 'banner_enabled')
|
const row = rows.find((item) => item.key === 'banner_enabled')
|
||||||
setBannerEnabledFromSettings(row ? isSwitchOn(row.value) : false)
|
setBannerEnabledFromSettings(row ? isSwitchOn(row.value) : false)
|
||||||
@@ -129,6 +136,13 @@ export function applyCustomerDirectSmsFromRows(rows: Array<{ key?: string; value
|
|||||||
setCustomerDirectSmsEnabledFromSettings(row ? isSwitchOn(row.value) : false)
|
setCustomerDirectSmsEnabledFromSettings(row ? isSwitchOn(row.value) : false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyConditionalOrderEnabledFromRows(
|
||||||
|
rows: Array<{ key?: string; value?: string | null }>
|
||||||
|
) {
|
||||||
|
const row = rows.find((item) => item.key === 'enable_conditional_order')
|
||||||
|
setConditionalOrderEnabledFromSettings(row ? isSwitchOn(row.value) : false)
|
||||||
|
}
|
||||||
|
|
||||||
async function bootstrapSettingsFromApi(): Promise<void> {
|
async function bootstrapSettingsFromApi(): Promise<void> {
|
||||||
if (settingsBootstrapped) return
|
if (settingsBootstrapped) return
|
||||||
|
|
||||||
@@ -145,6 +159,7 @@ async function bootstrapSettingsFromApi(): Promise<void> {
|
|||||||
applyBannerEnabledFromRows(list)
|
applyBannerEnabledFromRows(list)
|
||||||
applyTrackingSystemsEnabledFromRows(list)
|
applyTrackingSystemsEnabledFromRows(list)
|
||||||
applyCustomerDirectSmsFromRows(list)
|
applyCustomerDirectSmsFromRows(list)
|
||||||
|
applyConditionalOrderEnabledFromRows(list)
|
||||||
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))
|
||||||
@@ -153,6 +168,7 @@ async function bootstrapSettingsFromApi(): Promise<void> {
|
|||||||
setBannerEnabledFromSettings(false)
|
setBannerEnabledFromSettings(false)
|
||||||
setTrackingSystemsEnabledFromSettings(false)
|
setTrackingSystemsEnabledFromSettings(false)
|
||||||
setCustomerDirectSmsEnabledFromSettings(false)
|
setCustomerDirectSmsEnabledFromSettings(false)
|
||||||
|
setConditionalOrderEnabledFromSettings(false)
|
||||||
applyAccountingProviderFromRows([])
|
applyAccountingProviderFromRows([])
|
||||||
setCachedMainMetal(null)
|
setCachedMainMetal(null)
|
||||||
const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet')
|
const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet')
|
||||||
|
|||||||
Reference in New Issue
Block a user