added setting for users

This commit is contained in:
2026-08-16 22:00:48 +03:30
parent 84b0a4767a
commit 17dea2a52f
13 changed files with 635 additions and 29 deletions
+28
View File
@@ -0,0 +1,28 @@
import { useAxios } from '@core/utils/useAxios'
import type { TableInput } from '@core/type/baseTable'
export const getCustomerSpecialPricePackageTable = async (params: TableInput) =>
await useAxios.post('/admin/customer_special_price_packages/table', params)
export const getCustomerSpecialPricePackage = async (id: number) =>
await useAxios.get(`/admin/customer_special_price_packages/${id}`)
export const createCustomerSpecialPricePackage = async (payload: Record<string, unknown>) =>
await useAxios.post('/admin/customer_special_price_packages', payload)
export const updateCustomerSpecialPricePackage = async (id: number, payload: Record<string, unknown>) =>
await useAxios.put(`/admin/customer_special_price_packages/${id}`, payload)
export const deleteCustomerSpecialPricePackage = async (id: number) =>
await useAxios.delete(`/admin/customer_special_price_packages/${id}`)
export const toggleCustomerSpecialPricePackage = async (id: number) =>
await useAxios.put(`/admin/customer_special_price_packages/${id}/toggle`)
export const searchSpecialPriceCustomers = async (q: string) =>
await useAxios.get('/admin/customer_special_price_packages/search-customers', { params: { q } })
export const getProductsByCategories = async (category_names: string[]) =>
await useAxios.post('/admin/customer_special_price_packages/products-by-categories', {
category_names
})
+8 -2
View File
@@ -202,7 +202,7 @@ import SettingValueField from './SettingValueField.vue'
import SettingCard from './SettingCard.vue'
import { refreshAppModeMenu } from '@/router'
import { useUserStore } from '@/store/userStore'
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyProductFundsEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, applyPriceChannelAccessFromRows, applySupportChatMenuEnabledFromRows, applyGroupSmsFromRows, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setProductFundsEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setPriceChannelAccessFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings, setGroupSmsEnabledFromSettings } from '@/utils/config'
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyProductFundsEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, applyPriceChannelAccessFromRows, applySupportChatMenuEnabledFromRows, applyGroupSmsFromRows, applyCustomerSpecialPricingFromRows, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setProductFundsEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setPriceChannelAccessFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings, setGroupSmsEnabledFromSettings, setCustomerSpecialPricingEnabledFromSettings } from '@/utils/config'
import {
EDITABLE_SETTING_GROUPS,
SYSTEM_SETTING_GROUPS,
@@ -485,12 +485,14 @@ async function initConfig() {
applyPriceChannelAccessFromRows(baseData.value)
applySupportChatMenuEnabledFromRows(baseData.value)
applyGroupSmsFromRows(baseData.value)
applyCustomerSpecialPricingFromRows(baseData.value)
// در صفحهٔ عادی price_provider و show_to_users_chat و price_channel_access (can_edit=0) نیستند — از کل تنظیمات بخوان
if (
!baseData.value.some((row) => row.key === 'price_provider') ||
!baseData.value.some((row) => row.key === 'show_to_users_chat') ||
!baseData.value.some((row) => row.key === 'price_channel_access') ||
!baseData.value.some((row) => row.key === 'group_sms')
!baseData.value.some((row) => row.key === 'group_sms') ||
!baseData.value.some((row) => row.key === 'customer_special_pricing')
) {
try {
const { getConfiguration } = await import('@/api/configuration')
@@ -500,6 +502,7 @@ async function initConfig() {
applySupportChatMenuEnabledFromRows(rows)
applyPriceChannelAccessFromRows(rows)
applyGroupSmsFromRows(rows)
applyCustomerSpecialPricingFromRows(rows)
} catch {
/* کش قبلی می‌ماند */
}
@@ -572,6 +575,9 @@ function syncMenuFlagsFromSetting(key: string | undefined, value: string) {
if (key === 'group_sms') {
setGroupSmsEnabledFromSettings(isSwitchOn(value))
}
if (key === 'customer_special_pricing') {
setCustomerSpecialPricingEnabledFromSettings(isSwitchOn(value))
}
}
function onValueChange(row: Const, value: string) {
@@ -179,7 +179,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
title: 'دسترسی مشتری به قابلیت‌ها',
icon: 'vuesax-linear:people',
match: (k) =>
/^(enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|has_page_gift_card|enable_customer_level_profit|banner_enabled|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|tracking_systems_enabled|permision__enabled|show_to_users_chat)$/.test(
/^(enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|has_page_gift_card|enable_customer_level_profit|banner_enabled|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|customer_special_pricing|tracking_systems_enabled|permision__enabled|show_to_users_chat)$/.test(
k
)
},
@@ -322,6 +322,7 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
sms_notif_wallet_alert: 'switch',
sms_notif_support_admin: 'switch',
group_sms: 'switch',
customer_special_pricing: 'switch',
montzar_closed: 'switch',
montzar_open_buy: 'switch',
montzar_open_sell: 'switch',
@@ -825,6 +826,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
'روشن: وقتی مشتری اولین پیام خوانده‌نشده را در چت پشتیبانی می‌فرستد و ادمین همان گفتگو را باز و visible ندارد، پیامک به ادمین‌های دارای «ارسال پیامک» می‌رود. خاموش: اعلان پیامک چت نمی‌رود.',
group_sms:
'روشن: دکمه ارسال پیامک گروهی در پنل ادمین فعال است (ارسال ۱۰تایی). خاموش: ارسال پیامک گروهی غیرفعال است. فقط از تنظیمات پیشرفته.',
customer_special_pricing:
'روشن: زیرمنوی قیمت ویژه در مشتریان و اعمال سود خرید/فروش پکیج روی مظنه و سفارش برای کاربران انتخاب‌شده. خاموش: غیرفعال. فقط از دیپلوی قابل تغییر است.',
profit_gold_api_enabled:
'روشن: محاسبه سود طلا از API خارجی فعال است. خاموش: محاسبه سود از API خارجی انجام نمی‌شود (حالت پیش‌فرض اکثر نصب‌ها).',
main_wallet_products:
@@ -986,6 +989,7 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
open_register: 'ثبت‌نام باز',
sms_panel: 'پنل پیامک (کیف پول)',
customer_direct_sms: 'پیامک تکی به مشتری از لیست',
customer_special_pricing: 'قیمت ویژه مشتری',
charge_notif: 'پیامک شارژ ادمین',
sms_notif_order_admin: 'پیامک خرید/فروش به ادمین',
sms_notif_order_customer: 'پیامک خرید/فروش به مشتری',
@@ -1080,6 +1084,7 @@ export const SETTING_LOCKED_KEYS = new Set([
'fee_deduct_from_gold',
'referral_bonus_amount',
'group_sms',
'customer_special_pricing',
'customer_direct_sms',
'enable_conditional_order',
'enable_product_funds',
@@ -4,23 +4,27 @@
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
<template #action="{ record }">
<div class="flex items-center justify-center gap-2">
<a-tooltip placement="bottom">
<a-tooltip
placement="bottom"
class="cursor-pointer"
:get-popup-container="tooltipContainer"
@click="openAssetFunds(record)"
>
<template #title><span>قلک‌های این دارایی</span></template>
<Icon
class="cursor-pointer"
icon="vuesax-linear:safe-home"
color="#7C3AED"
@click="openAssetFunds(record)"
/>
<span class="inline-flex">
<Icon icon="vuesax-linear:safe-home" color="#7C3AED" />
</span>
</a-tooltip>
<a-tooltip placement="bottom">
<a-tooltip
placement="bottom"
class="cursor-pointer"
:get-popup-container="tooltipContainer"
@click="openAssetTx(record)"
>
<template #title><span>تراکنش‌های این دارایی</span></template>
<Icon
class="cursor-pointer"
icon="vuesax-linear:receipt-2"
color="#4572E9"
@click="openAssetTx(record)"
/>
<span class="inline-flex">
<Icon icon="vuesax-linear:receipt-2" color="#4572E9" />
</span>
</a-tooltip>
</div>
</template>
@@ -59,6 +63,7 @@ import {
} from '@/api/user'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import BasicShowContext from '@/components/BasicShowContext.vue'
import { toJalali } from '@core/utils/useDataTime'
const [submit, reload] = useBaseTable()
const [assetFundsSubmit] = useBaseTable()
@@ -106,7 +111,13 @@ const assetTxCols = [
{ title: 'نوع', dataIndex: 'type_str', key: 'type_str', type: 'Text' },
{ title: 'طلا', dataIndex: 'gold', key: 'gold', type: 'Number' },
{ title: 'ریال', dataIndex: 'cash', key: 'cash', type: 'Number' },
{ title: 'تاریخ', dataIndex: 'created_at', key: 'created_at', type: 'Text' }
{
title: 'تاریخ',
dataIndex: 'created_at',
key: 'created_at',
type: 'DateTime',
customRender: (r: any) => (r.record?.created_at ? toJalali(r.record.created_at) : '—')
}
]
const assetTxTblProps = computed(() => ({
@@ -133,8 +144,17 @@ function openAssetTx(record: any) {
assetTxVisible.value = true
}
const tooltipContainer = () => document.body
const fetch = () => reload()
defineExpose({ fetch })
onMounted(() => {})
</script>
<style lang="less">
.ant-popover,
.ant-tooltip {
z-index: 1001 !important;
}
</style>
@@ -4,14 +4,16 @@
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
<template #action="{ record }">
<div class="flex items-center justify-center gap-2">
<a-tooltip placement="bottom">
<a-tooltip
placement="bottom"
class="cursor-pointer"
:get-popup-container="tooltipContainer"
@click="openDetails(record)"
>
<template #title><span>مشخصات / تراکنش‌ها</span></template>
<Icon
class="cursor-pointer"
icon="vuesax-linear:info-circle"
color="#4572E9"
@click="openDetails(record)"
/>
<span class="inline-flex">
<Icon icon="vuesax-linear:info-circle" color="#4572E9" />
</span>
</a-tooltip>
</div>
</template>
@@ -96,6 +98,15 @@ function openDetails(record: any) {
detailsVisible.value = true
}
const tooltipContainer = () => document.body
const fetch = () => reload()
defineExpose({ fetch })
</script>
<style lang="less">
.ant-popover,
.ant-tooltip {
z-index: 1001 !important;
}
</style>
+13 -4
View File
@@ -21,14 +21,23 @@ import { computed } from 'vue'
import BasicMenu from '@/components/BasicMenu/index.vue'
import { useRouterStore } from '@core/stores/router'
import { get } from 'lodash'
import { customerSpecialPricingEnabledSetting, initWalletSettingsFromApi } from '@/utils/config'
initWalletSettingsFromApi()
const routerStore = useRouterStore()
const menu = computed(() => {
const item = routerStore.routes.find((item) => item.name === 'customers')
const children = get(item, 'children[0].children', []) as Array<{ meta?: { order?: number } }>
return [...children].sort(
(a, b) => (a.meta?.order ?? 99) - (b.meta?.order ?? 99)
)
const children = get(item, 'children[0].children', []) as Array<{
name?: string
meta?: { order?: number }
}>
return [...children]
.filter(
(child) =>
child.name !== 'customerMain_specialPrice' || customerSpecialPricingEnabledSetting.value
)
.sort((a, b) => (a.meta?.order ?? 99) - (b.meta?.order ?? 99))
})
</script>
@@ -0,0 +1,170 @@
<template>
<a-form :model="form" ref="formRef" layout="vertical" class="space-y-3">
<a-form-item label="عنوان پکیج" name="title" :rules="[{ required: true, message: 'عنوان الزامی است' }]">
<BasicInput v-model:value="form.title" placeholder="مثلاً مشتریان VIP" />
</a-form-item>
<a-form-item label="گروه دسته‌بندی" name="category_names" :rules="[{ required: true, type: 'array', min: 1, message: 'دسته را انتخاب کنید' }]">
<BasicSelect
v-model:value="form.category_names"
:items="categoryItems"
valueName="value"
keyName="key"
mode="multiple"
class="w-full"
placeholder="یک یا چند دسته"
/>
</a-form-item>
<a-form-item label="محصول" name="product_ids" :rules="[{ required: true, type: 'array', min: 1, message: 'محصول را انتخاب کنید' }]">
<BasicSelect
v-model:value="form.product_ids"
:items="productItems"
valueName="title"
keyName="id"
mode="multiple"
class="w-full"
:disabled="!form.category_names?.length"
placeholder="ابتدا دسته را انتخاب کنید"
/>
</a-form-item>
<a-form-item label="لیست کاربران زیر مجموعه" name="customer_ids" :rules="[{ required: true, type: 'array', min: 1, message: 'کاربر را انتخاب کنید' }]">
<a-select
v-model:value="form.customer_ids"
mode="multiple"
class="w-full"
placeholder="جستجو نام یا شماره…"
show-search
:filter-option="false"
:options="customerOptions"
@search="onCustomerSearch"
/>
</a-form-item>
<div class="grid gap-3 md:grid-cols-2">
<a-form-item label="سود فروش" name="profit_sell">
<BasicInput v-model:value="form.profit_sell" type="price" addon-before="ریال" />
</a-form-item>
<a-form-item label="سود خرید" name="profit_buy">
<BasicInput v-model:value="form.profit_buy" type="price" addon-before="ریال" />
</a-form-item>
</div>
<a-form-item label="نمایش محصول">
<a-switch v-model:checked="form.show_product">
<template #checkedChildren>فعال</template>
<template #unCheckedChildren>مخفی</template>
</a-switch>
<p class="m-0 mt-1 text-xs text-gray-500">
خاموش = این محصولات برای کاربران پکیج در اپ مخفی می‌شوند.
</p>
</a-form-item>
<a-form-item label="وضعیت پکیج">
<a-switch v-model:checked="form.is_active">
<template #checkedChildren>فعال</template>
<template #unCheckedChildren>غیرفعال</template>
</a-switch>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import type { FormInstance } from 'ant-design-vue'
import { debounce } from 'lodash'
import { onMounted, ref, watch } from 'vue'
import BasicInput from '@/components/BasicInput.vue'
import BasicSelect from '@/components/BasicSelect.vue'
import { syncAndLoadCategories, type CategorySelectItem } from '@/utils/categorySelect'
import {
getProductsByCategories,
searchSpecialPriceCustomers
} from '@/api/customerSpecialPrice'
export type SpecialPriceForm = {
title: string
category_names: string[]
product_ids: number[]
customer_ids: number[]
profit_buy: number | string
profit_sell: number | string
show_product: boolean
is_active: boolean
}
const props = defineProps<{
form: SpecialPriceForm
}>()
const formRef = ref<FormInstance>()
const categoryItems = ref<CategorySelectItem[]>([])
const productItems = ref<Array<{ id: number; title: string }>>([])
const customerOptions = ref<Array<{ value: number; label: string }>>([])
async function loadCategories() {
categoryItems.value = await syncAndLoadCategories({ selectOnly: true })
}
async function loadProducts() {
const names = props.form.category_names || []
if (!names.length) {
productItems.value = []
props.form.product_ids = []
return
}
const res: any = await getProductsByCategories(names)
const rows = res?.products ?? res?.data?.products ?? res ?? []
productItems.value = (Array.isArray(rows) ? rows : []).map((p: any) => ({
id: Number(p.id),
title: `${p.title || '-'} (${p.group_type || '-'})`
}))
const allowed = new Set(productItems.value.map((p) => p.id))
props.form.product_ids = (props.form.product_ids || []).filter((id) => allowed.has(Number(id)))
}
const onCustomerSearch = debounce(async (q: string) => {
const res: any = await searchSpecialPriceCustomers(q || '')
const rows = res?.customers ?? res?.data?.customers ?? []
const mapped = (Array.isArray(rows) ? rows : []).map((c: any) => ({
value: Number(c.id),
label: `${c.name || '-'} — ${c.mobile || ''} (#${c.id})`
}))
const selected = new Set((props.form.customer_ids || []).map(Number))
for (const item of customerOptions.value) {
if (selected.has(item.value) && !mapped.some((m) => m.value === item.value)) {
mapped.push(item)
}
}
customerOptions.value = mapped
}, 300)
watch(
() => [...(props.form.category_names || [])],
() => {
void loadProducts()
}
)
onMounted(async () => {
await loadCategories()
await loadProducts()
await onCustomerSearch('')
})
defineExpose({
validate: () => formRef.value?.validate(),
seedCustomers: (rows: Array<{ id: number; name?: string; mobile?: string }>) => {
customerOptions.value = rows.map((c) => ({
value: Number(c.id),
label: `${c.name || '-'} — ${c.mobile || ''} (#${c.id})`
}))
},
seedProducts: (rows: Array<{ id: number; title?: string; group_type?: string }>) => {
productItems.value = rows.map((p) => ({
id: Number(p.id),
title: `${p.title || '-'} (${p.group_type || '-'})`
}))
}
})
</script>
@@ -0,0 +1,74 @@
<template>
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
<BasicShowContext title="پکیج‌های قیمت ویژه">
<template v-if="isMobile" #extra>
<BaseButton type="primary" size="small" icon="vuesax-linear:add" @click="emits('add')">
افزودن
</BaseButton>
</template>
<BaseTable v-bind="tblProps" @register="submit" class="h-full min-h-0">
<template #extraHeader>
<BaseButton type="primary" size="small" icon="vuesax-linear:add" @click="emits('add')">
افزودن پکیج
</BaseButton>
</template>
<template #action="{ record }">
<div class="flex items-center justify-center gap-2">
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('edit', record)">
<template #title><span>ویرایش</span></template>
<Icon icon="vuesax-linear:edit-2" color="#4572E9" />
</a-tooltip>
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('toggle', record)">
<template #title>
<span>{{ record.is_active ? 'غیرفعال کردن' : 'فعال کردن' }}</span>
</template>
<Icon
:icon="record.is_active ? 'vuesax-linear:toggle-on-circle' : 'vuesax-linear:toggle-off-circle'"
:color="record.is_active ? '#54AD06' : '#ad0627'"
/>
</a-tooltip>
<a-popconfirm
title="پکیج حذف شود؟"
ok-text="تایید"
cancel-text="انصراف"
@confirm="emits('delete', record)"
>
<a-tooltip placement="bottom" class="cursor-pointer">
<template #title><span>حذف</span></template>
<Icon icon="vuesax-linear:trash" color="#C53030" />
</a-tooltip>
</a-popconfirm>
</div>
</template>
</BaseTable>
</BasicShowContext>
</div>
</template>
<script setup lang="ts">
import type { TableInput } from '@core/type/baseTable'
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import BasicShowContext from '@/components/BasicShowContext.vue'
import { getCustomerSpecialPricePackageTable } from '@/api/customerSpecialPrice'
import { getSpecialPricePackageColumns } from '../hooks/specialPriceColumns'
import { isMobile } from '@core/utils/is'
import { ref } from 'vue'
const emits = defineEmits<{
(e: 'add'): void
(e: 'edit', record: any): void
(e: 'toggle', record: any): void
(e: 'delete', record: any): void
}>()
const [submit, reload] = useBaseTable()
const tblProps = ref({
columns: getSpecialPricePackageColumns(),
api: (params: TableInput) => getCustomerSpecialPricePackageTable(params)
})
defineExpose({ fetch: reload })
</script>
@@ -0,0 +1,79 @@
import type { BaseTableColumn } from '@core/type/baseTable'
import { usePrice } from '@core/utils/usePrice'
import { toJalali } from '@core/utils/useDataTime'
export function getSpecialPricePackageColumns(): BaseTableColumn[] {
return [
{
title: 'ردیف',
dataIndex: 'id',
key: 'row',
filterable: false,
sortable: false,
customRender: (record: any) => record.index + 1
},
{
title: 'عنوان پکیج',
dataIndex: 'title',
key: 'title',
type: 'Text'
},
{
title: 'تعداد کاربر',
dataIndex: 'customers_count',
key: 'customers_count',
type: 'Number',
filterable: false
},
{
title: 'تعداد محصول',
dataIndex: 'products_count',
key: 'products_count',
type: 'Number',
filterable: false
},
{
title: 'سود خرید',
dataIndex: 'profit_buy',
key: 'profit_buy',
type: 'Text',
customRender: ({ record }) => usePrice(record.profit_buy ?? 0, true, '')
},
{
title: 'سود فروش',
dataIndex: 'profit_sell',
key: 'profit_sell',
type: 'Text',
customRender: ({ record }) => usePrice(record.profit_sell ?? 0, true, '')
},
{
title: 'نمایش محصول',
dataIndex: 'show_product',
key: 'show_product',
type: 'Text',
customRender: ({ record }) => (record.show_product ? 'فعال' : 'مخفی')
},
{
title: 'وضعیت',
dataIndex: 'is_active',
key: 'is_active',
type: 'Text',
customRender: ({ record }) => (record.is_active ? 'فعال' : 'غیرفعال')
},
{
title: 'تاریخ',
dataIndex: 'updated_at',
key: 'updated_at',
type: 'DateTime',
customRender: (r: any) => (r.record?.updated_at ? toJalali(r.record.updated_at) : '—')
},
{
title: 'عملیات',
dataIndex: 'action',
key: 'action',
slot: 'action',
filterable: false,
sortable: false
}
]
}
@@ -0,0 +1,179 @@
<template>
<div class="flex h-full w-full flex-col">
<tbl
ref="tableRef"
class="h-full w-full"
@add="openAdd"
@edit="openEdit"
@toggle="onToggle"
@delete="onDelete"
/>
<BasicActionSheet
v-model:visible="editorVisible"
:title="sheetTitle"
modal-width="40rem"
drawer-height="90vh"
:loading="loading"
:show-button="true"
:show-cancel="true"
:show-ok="true"
ok-text="ثبت"
cancel-text="انصراف"
@ok="submitForm"
@close="closeEditor"
@cancel="closeEditor"
>
<SpecialPriceEditorForm ref="editorRef" :form="form" />
</BasicActionSheet>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { message, Modal } from 'ant-design-vue'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import tbl from './components/table.vue'
import SpecialPriceEditorForm, {
type SpecialPriceForm
} from './components/SpecialPriceEditorForm.vue'
import {
createCustomerSpecialPricePackage,
deleteCustomerSpecialPricePackage,
getCustomerSpecialPricePackage,
toggleCustomerSpecialPricePackage,
updateCustomerSpecialPricePackage
} from '@/api/customerSpecialPrice'
const tableRef = ref<null | { fetch: () => void }>(null)
const editorRef = ref<null | {
validate: () => Promise<void>
seedCustomers: (rows: any[]) => void
seedProducts: (rows: any[]) => void
}>(null)
const editorVisible = ref(false)
const loading = ref(false)
const editId = ref<number | null>(null)
const emptyForm = (): SpecialPriceForm => ({
title: '',
category_names: [],
product_ids: [],
customer_ids: [],
profit_buy: 0,
profit_sell: 0,
show_product: true,
is_active: true
})
const form = ref<SpecialPriceForm>(emptyForm())
const sheetTitle = computed(() => (editId.value ? 'ویرایش پکیج قیمت ویژه' : 'افزودن پکیج قیمت ویژه'))
function openAdd() {
editId.value = null
form.value = emptyForm()
editorVisible.value = true
}
async function openEdit(record: any) {
loading.value = true
try {
const res: any = await getCustomerSpecialPricePackage(Number(record.id))
const data = res?.data ?? res
editId.value = Number(record.id)
form.value = {
title: data?.package?.title ?? '',
category_names: data?.category_names ?? [],
product_ids: (data?.product_ids ?? []).map(Number),
customer_ids: (data?.customer_ids ?? []).map(Number),
profit_buy: data?.package?.profit_buy ?? 0,
profit_sell: data?.package?.profit_sell ?? 0,
show_product: Boolean(data?.package?.show_product ?? true),
is_active: Boolean(data?.package?.is_active ?? true)
}
editorVisible.value = true
await Promise.resolve()
editorRef.value?.seedCustomers(data?.customers ?? [])
editorRef.value?.seedProducts(data?.products ?? [])
} catch (e: any) {
message.error(e?.response?.data?.message ?? 'خطا در دریافت پکیج')
} finally {
loading.value = false
}
}
function closeEditor() {
editorVisible.value = false
editId.value = null
form.value = emptyForm()
}
function showWarnings(warnings?: string[]) {
if (!warnings?.length) return
Modal.warning({
title: 'هشدار تداخل پکیج',
content: warnings.slice(0, 8).join('\n'),
width: 520,
okText: 'متوجه شدم'
})
}
async function submitForm() {
try {
await editorRef.value?.validate()
} catch {
return
}
loading.value = true
try {
const payload = {
title: form.value.title,
category_names: form.value.category_names,
product_ids: form.value.product_ids.map(Number),
customer_ids: form.value.customer_ids.map(Number),
profit_buy: form.value.profit_buy,
profit_sell: form.value.profit_sell,
show_product: form.value.show_product,
is_active: form.value.is_active
}
let res: any
if (editId.value) {
res = await updateCustomerSpecialPricePackage(editId.value, payload)
message.success(res?.message ?? 'به‌روز شد')
} else {
res = await createCustomerSpecialPricePackage(payload)
message.success(res?.message ?? 'ثبت شد')
}
showWarnings(res?.warnings ?? res?.data?.warnings)
closeEditor()
tableRef.value?.fetch()
} catch (e: any) {
message.error(e?.response?.data?.message ?? 'خطا در ذخیره')
} finally {
loading.value = false
}
}
async function onToggle(record: any) {
try {
const res: any = await toggleCustomerSpecialPricePackage(Number(record.id))
message.success(res?.message ?? 'وضعیت تغییر کرد')
tableRef.value?.fetch()
} catch (e: any) {
message.error(e?.response?.data?.message ?? 'خطا')
}
}
async function onDelete(record: any) {
try {
await deleteCustomerSpecialPricePackage(Number(record.id))
message.success('حذف شد')
tableRef.value?.fetch()
} catch (e: any) {
message.error(e?.response?.data?.message ?? 'خطا در حذف')
}
}
</script>
+10
View File
@@ -70,6 +70,16 @@ const Customers: RouteRecordRaw = {
title: 'احراز هویت',
order: 3
}
},
{
name: 'customerMain_specialPrice',
path: 'special-price',
component: () => import('@/pages/menuPages/customers/specialPrice/index.vue'),
meta: {
icon: 'vuesax-linear:dollar-circle',
title: 'قیمت ویژه',
order: 4
}
}
]
}
+15
View File
@@ -58,6 +58,9 @@ export const priceTriggerEnabledSetting = ref(false)
/** reactive — دکمه ارسال پیامک گروهی در صفحه پیامک‌ها */
export const groupSmsEnabledSetting = ref(false)
/** reactive — زیرمنوی قیمت ویژه مشتری */
export const customerSpecialPricingEnabledSetting = ref(false)
/** reactive — منو و زنگوله اعلان داخل اپ */
export const appNotificationEnabledSetting = ref(true)
@@ -156,6 +159,10 @@ export function setGroupSmsEnabledFromSettings(value: boolean): void {
groupSmsEnabledSetting.value = value
}
export function setCustomerSpecialPricingEnabledFromSettings(value: boolean): void {
customerSpecialPricingEnabledSetting.value = value
}
export function setConditionalOrderEnabledFromSettings(value: boolean): void {
conditionalOrderEnabledSetting.value = value
}
@@ -262,6 +269,13 @@ export function applyGroupSmsFromRows(rows: Array<{ key?: string; value?: string
if (row) setGroupSmsEnabledFromSettings(isSwitchOn(row.value))
}
export function applyCustomerSpecialPricingFromRows(
rows: Array<{ key?: string; value?: string | null }>
) {
const row = rows.find((item) => item.key === 'customer_special_pricing')
if (row) setCustomerSpecialPricingEnabledFromSettings(isSwitchOn(row.value))
}
export function applyConditionalOrderEnabledFromRows(
rows: Array<{ key?: string; value?: string | null }>
) {
@@ -330,6 +344,7 @@ async function bootstrapSettingsFromApi(): Promise<void> {
applyTrackingSystemsEnabledFromRows(list)
applyCustomerDirectSmsFromRows(list)
applyGroupSmsFromRows(list)
applyCustomerSpecialPricingFromRows(list)
applyConditionalOrderEnabledFromRows(list)
applyProductFundsEnabledFromRows(list)
applyCustomerLevelProfitEnabledFromRows(list)