change gift-card

This commit is contained in:
2026-09-21 11:10:34 +03:30
parent 8004dd52b3
commit 7e869902d6
7 changed files with 214 additions and 48 deletions
+17 -2
View File
@@ -1,6 +1,16 @@
export interface GiftcardCustomer {
id: number
name: string
mobile: string
}
export interface Giftcard { export interface Giftcard {
id: number id: number
customer_id: any customer_id: any
name?: string
mobile?: string
customer?: GiftcardCustomer
show_in_app?: number | boolean
count: number count: number
amount: number amount: number
total: number total: number
@@ -35,9 +45,14 @@ export interface AssignedTo {
mobile: string mobile: string
} }
export type GiftCardAssignMode = 'customer' | 'public'
export interface GiftcardInput { export interface GiftcardInput {
mobile: string assignMode: GiftCardAssignMode
name: string customer_id?: number
show_in_app?: number
mobile?: string
name?: string
count: number count: number
amount: number amount: number
unit: string unit: string
@@ -12,7 +12,13 @@
@close="close" @close="close"
> >
<div class="mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div class="mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0"> <div class="min-w-0 space-y-1">
<p v-if="giftCardCustomer" class="m-0 text-13px font-600" style="color: var(--admin-text)">
مشتری: {{ giftCardCustomer.name }} — <span dir="ltr">{{ giftCardCustomer.mobile }}</span>
</p>
<p v-else class="m-0 text-13px font-600" style="color: var(--admin-text)">
کد عمومی — بدون مشتری مشخص
</p>
<p class="m-0 text-13px text-[var(--admin-text-secondary)]"> <p class="m-0 text-13px text-[var(--admin-text-secondary)]">
{{ codes.length }} کد — دانلود به صورت اکسل یا PDF {{ codes.length }} کد — دانلود به صورت اکسل یا PDF
</p> </p>
@@ -102,6 +108,7 @@ import {
downloadGiftCardCodesPdf, downloadGiftCardCodesPdf,
type GiftCardCodesExportMeta type GiftCardCodesExportMeta
} from '../utils/exportGiftCardCodes' } from '../utils/exportGiftCardCodes'
import { resolveGiftCardCustomer } from '../utils/giftCardCustomer'
import { SELECT_DROPDOWN_Z_INDEX } from '@/constants/overlayZIndex' import { SELECT_DROPDOWN_Z_INDEX } from '@/constants/overlayZIndex'
const props = defineProps<{ const props = defineProps<{
@@ -129,20 +136,22 @@ const columns = [
{ title: 'وضعیت', dataIndex: 'sts', key: 'sts' }, { title: 'وضعیت', dataIndex: 'sts', key: 'sts' },
{ title: 'تاریخ انقضا', dataIndex: 'expiry_date', key: 'expiry_date' }, { title: 'تاریخ انقضا', dataIndex: 'expiry_date', key: 'expiry_date' },
{ title: 'تاریخ استفاده', dataIndex: 'used_at', key: 'used_at' }, { title: 'تاریخ استفاده', dataIndex: 'used_at', key: 'used_at' },
{ title: 'نام کاربر اختصاص داده شده', dataIndex: 'assigned_to.name', key: 'assigned_to_name' }, { title: 'نام استفاده‌کننده', dataIndex: 'assigned_to.name', key: 'assigned_to_name' },
{ title: 'شماره موبایل کاربر اختصاص داده شده', dataIndex: 'assigned_to.mobile', key: 'assigned_to_mobile' } { title: 'موبایل استفاده‌کننده', dataIndex: 'assigned_to.mobile', key: 'assigned_to_mobile' }
] ]
function close() { function close() {
visibleProxy.value = false visibleProxy.value = false
} }
const giftCardCustomer = computed(() => resolveGiftCardCustomer(props.giftCard))
const exportMeta = computed<GiftCardCodesExportMeta>(() => ({ const exportMeta = computed<GiftCardCodesExportMeta>(() => ({
id: props.giftCard?.id, id: props.giftCard?.id,
unit: props.giftCard?.unit, unit: props.giftCard?.unit,
count: props.giftCard?.count, count: props.giftCard?.count,
customerName: (props.giftCard as Giftcard & { customer?: { name?: string } })?.customer?.name, customerName: giftCardCustomer.value?.name,
customerMobile: (props.giftCard as Giftcard & { customer?: { mobile?: string } })?.customer?.mobile, customerMobile: giftCardCustomer.value?.mobile,
createdAt: props.giftCard?.created_at createdAt: props.giftCard?.created_at
})) }))
@@ -1,25 +1,48 @@
<template> <template>
<a-form :model="form" ref="formRef" autocomplete="off"> <a-form :model="props.form" ref="formRef" autocomplete="off">
<div class="space-y-3"> <div class="space-y-3">
<a-form-item name="mobile" :rules="inputLenRules(11)"> <a-form-item name="assignMode" :rules="inputRules()">
<template #label> <template #label>
<AdminLabelWithHelp label="شماره موبایل مشتری" :help="giftCardMobileHelp" /> <AdminLabelWithHelp label="نوع ثبت" :help="giftCardAssignModeHelp" />
</template> </template>
<BasicInput v-model:value="form.mobile" type="tel" placeholder="09123456789" /> <a-radio-group v-model:value="props.form.assignMode" button-style="solid" @change="onAssignModeChange">
<a-radio-button value="public">کد عمومی (بدون مشتری)</a-radio-button>
<a-radio-button value="customer">اختصاص به مشتری</a-radio-button>
</a-radio-group>
</a-form-item> </a-form-item>
<a-form-item name="name" :rules="inputRules()"> <a-form-item v-if="isPublicMode" name="show_in_app" :rules="inputRules()">
<template #label> <template #label>
<AdminLabelWithHelp label="نام مشتری" :help="giftCardNameHelp" /> <AdminLabelWithHelp label="نمایش در اپ کاربر" :help="giftCardPublicVisibilityHelp" />
</template> </template>
<BasicInput v-model:value="form.name" placeholder="نام و نام خانوادگی مشتری" /> <a-radio-group v-model:value="props.form.show_in_app" button-style="solid" class="flex flex-col gap-2 sm:flex-row">
<a-radio-button :value="1">نمایش برای همه</a-radio-button>
<a-radio-button :value="0">مخفی — فقط با وارد کردن کد</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item v-if="isCustomerMode" name="customer_id" :rules="customerIdRules">
<template #label>
<AdminLabelWithHelp label="مشتری" :help="giftCardCustomerSelectHelp" />
</template>
<a-select
v-model:value="props.form.customer_id"
show-search
allow-clear
placeholder="انتخاب مشتری"
class="w-full"
:options="customerOptions"
:loading="customersLoading"
:filter-option="filterCustomer"
:dropdown-style="{ zIndex: SELECT_DROPDOWN_Z_INDEX }"
/>
</a-form-item> </a-form-item>
<a-form-item name="unit" :rules="inputRules()"> <a-form-item name="unit" :rules="inputRules()">
<template #label> <template #label>
<AdminLabelWithHelp label="نوع کارت هدیه" :help="giftCardUnitHelp" /> <AdminLabelWithHelp label="نوع کارت هدیه" :help="giftCardUnitHelp" />
</template> </template>
<a-radio-group v-model:value="form.unit" button-style="solid"> <a-radio-group v-model:value="props.form.unit" button-style="solid">
<a-radio-button value="1">ریالی</a-radio-button> <a-radio-button value="1">ریالی</a-radio-button>
<a-radio-button value="2">{{ metalLabel.goldWalletShort() }}</a-radio-button> <a-radio-button value="2">{{ metalLabel.goldWalletShort() }}</a-radio-button>
</a-radio-group> </a-radio-group>
@@ -29,19 +52,19 @@
<template #label> <template #label>
<AdminLabelWithHelp label="تعداد کدها" :help="giftCardCountHelp" /> <AdminLabelWithHelp label="تعداد کدها" :help="giftCardCountHelp" />
</template> </template>
<BasicInput v-model:value="form.count" type="tel" placeholder="تعداد کدهای کارت هدیه" /> <BasicInput v-model:value="props.form.count" type="tel" placeholder="تعداد کدهای کارت هدیه" />
</a-form-item> </a-form-item>
<a-form-item name="amount" :rules="inputRulesNumber()"> <a-form-item name="amount" :rules="inputRulesNumber()">
<template #label> <template #label>
<AdminLabelWithHelp <AdminLabelWithHelp
:label="`مقدار ${form.unit === '2' ? '(گرم)' : '(ریال)'}`" :label="`مقدار ${props.form.unit === '2' ? '(گرم)' : '(ریال)'}`"
:help="giftCardAmountHelp" :help="giftCardAmountHelp"
/> />
</template> </template>
<BasicInput <BasicInput
v-model:value="form.amount" v-model:value="props.form.amount"
:addon-before="form.unit === '2' ? 'گرم' : 'ریال'" :addon-before="props.form.unit === '2' ? 'گرم' : 'ریال'"
type="tel" type="tel"
placeholder="مقدار کارت هدیه" placeholder="مقدار کارت هدیه"
/> />
@@ -51,11 +74,11 @@
<template #label> <template #label>
<AdminLabelWithHelp label="تاریخ انقضا" :help="giftCardExpiryHelp" /> <AdminLabelWithHelp label="تاریخ انقضا" :help="giftCardExpiryHelp" />
</template> </template>
<BaseDatePicker v-model:value="form.expiry_date" format="YYYY-MM-DD" /> <BaseDatePicker v-model:value="props.form.expiry_date" format="YYYY-MM-DD" />
</a-form-item> </a-form-item>
<a-form-item label="توضیحات" name="description" class="mb-0"> <a-form-item label="توضیحات" name="description" class="mb-0">
<BasicTextArea v-model:value="form.description" place-holder="توضیحات اختیاری — فقط برای یادداشت شما در پنل" /> <BasicTextArea v-model:value="props.form.description" place-holder="توضیحات اختیاری — فقط برای یادداشت شما در پنل" />
</a-form-item> </a-form-item>
</div> </div>
</a-form> </a-form>
@@ -64,24 +87,55 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance } from 'ant-design-vue' import type { FormInstance } from 'ant-design-vue'
import type { GiftcardInput } from '@/model/giftCard' import type { GiftcardInput } from '@/model/giftCard'
import { getCustomersList } from '@/api/customer'
import type { Customer } from '@/model/customer'
import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue' import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
import { import {
giftCardAmountHelp, giftCardAmountHelp,
giftCardAssignModeHelp,
giftCardCountHelp, giftCardCountHelp,
giftCardCustomerSelectHelp,
giftCardExpiryHelp, giftCardExpiryHelp,
giftCardMobileHelp, giftCardPublicVisibilityHelp,
giftCardNameHelp,
giftCardUnitHelp, giftCardUnitHelp,
} from '@/utils/adminHelpCopy' } from '@/utils/adminHelpCopy'
import { inputLenRules, inputRules, inputRulesNumber } from '@/utils/inputRules' import { SELECT_DROPDOWN_Z_INDEX } from '@/constants/overlayZIndex'
import { inputRules, inputRulesNumber } from '@/utils/inputRules'
import { metalLabel } from '@/utils/adminMetalLabels' import { metalLabel } from '@/utils/adminMetalLabels'
import dayjs from 'dayjs' import dayjs from 'dayjs'
defineProps<{ const props = defineProps<{
form: GiftcardInput form: GiftcardInput
}>() }>()
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const customers = ref<Customer[]>([])
const customersLoading = ref(false)
const isCustomerMode = computed(() => props.form.assignMode === 'customer')
const isPublicMode = computed(() => props.form.assignMode === 'public')
const customerOptions = computed(() =>
customers.value.map((customer) => ({
value: customer.id,
label: `${customer.name ?? '—'} — ${customer.mobile ?? ''}`.trim(),
}))
)
const customerIdRules = [
{
validator(_rule: unknown, value: unknown) {
if (props.form.assignMode !== 'customer') {
return Promise.resolve()
}
if (value === undefined || value === null || value === '') {
return Promise.reject('انتخاب مشتری الزامی است')
}
return Promise.resolve()
},
trigger: ['change', 'blur'],
},
]
const expiryDateRules = [ const expiryDateRules = [
...inputRules(), ...inputRules(),
@@ -103,6 +157,39 @@ const expiryDateRules = [
}, },
] ]
function filterCustomer(input: string, option?: { label?: string }) {
return String(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
function onAssignModeChange() {
if (props.form.assignMode === 'public') {
props.form.customer_id = undefined
if (props.form.show_in_app === undefined) {
props.form.show_in_app = 0
}
formRef.value?.clearValidate?.(['customer_id'])
return
}
props.form.show_in_app = 0
formRef.value?.clearValidate?.(['show_in_app'])
}
async function loadCustomers() {
customersLoading.value = true
try {
customers.value = (await getCustomersList()) ?? []
} catch (e) {
console.error(e)
} finally {
customersLoading.value = false
}
}
onMounted(() => {
void loadCustomers()
})
defineExpose({ defineExpose({
validateFields: () => formRef.value?.validateFields(), validateFields: () => formRef.value?.validateFields(),
resetFields: () => formRef.value?.resetFields(), resetFields: () => formRef.value?.resetFields(),
@@ -3,6 +3,7 @@ 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 { metalLabel } from '@/utils/adminMetalLabels' import { metalLabel } from '@/utils/adminMetalLabels'
import { giftCardCustomerLabel, giftCardCustomerMobile } from '../utils/giftCardCustomer'
/*import BudgetSelect from '@/components/BudgetSelect.vue' /*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore' import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect' import type { FilterSelect } from '@/model/filterSelect'
@@ -105,18 +106,14 @@ export function getGiftCardColumns(): BaseTableColumn[] {
dataIndex: 'customer.name', dataIndex: 'customer.name',
key: 'customer.name', key: 'customer.name',
type: 'Text', type: 'Text',
customRender: ({ record }) => { customRender: ({ record }) => giftCardCustomerLabel(record)
return record.customer?.name ?? '-'
}
}, },
{ {
title: 'شماره موبایل مشتری', title: 'شماره موبایل مشتری',
dataIndex: 'customer.mobile', dataIndex: 'customer.mobile',
key: 'customer.mobile', key: 'customer.mobile',
type: 'Number', type: 'Number',
customRender: ({ record }) => { customRender: ({ record }) => giftCardCustomerMobile(record)
return record.customer?.mobile ?? '-'
}
}, },
{ {
title: 'توضیحات', title: 'توضیحات',
+35 -12
View File
@@ -135,6 +135,7 @@ import {
discountPercentHelp, discountPercentHelp,
} from '@/utils/adminHelpCopy' } from '@/utils/adminHelpCopy'
import BaseDatePicker from '@core/components/BaseDatePicker' import BaseDatePicker from '@core/components/BaseDatePicker'
import { giftCardCustomerLabel, giftCardCustomerMobile } from './utils/giftCardCustomer'
//loading //loading
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
@@ -169,8 +170,9 @@ function closeGiftCardSheet() {
//form //form
const form = ref<GiftcardInput>({ const form = ref<GiftcardInput>({
mobile: '', assignMode: 'public',
name: '', customer_id: undefined,
show_in_app: 0,
count: undefined, count: undefined,
amount: undefined, amount: undefined,
unit: '1', unit: '1',
@@ -187,8 +189,8 @@ const selectedGiftCard = ref<Giftcard | null>(null)
function info(val: Giftcard) { function info(val: Giftcard) {
// نمایش اطلاعات اصلی در BasicInfoModal // نمایش اطلاعات اصلی در BasicInfoModal
infoParams.title = 'اطلاعات کارت هدیه' infoParams.title = 'اطلاعات کارت هدیه'
infoParams.name = val.customer?.name ?? '-' infoParams.name = giftCardCustomerLabel(val)
infoParams.type = val.customer?.mobile ?? '-' infoParams.type = giftCardCustomerMobile(val)
infoParams.date = toJalaliNoTime(val.created_at) infoParams.date = toJalaliNoTime(val.created_at)
infoParams.time = toTime(val.created_at) infoParams.time = toTime(val.created_at)
infoParams.itemsTitle = 'اطلاعات کارت هدیه' infoParams.itemsTitle = 'اطلاعات کارت هدیه'
@@ -197,6 +199,18 @@ function info(val: Giftcard) {
key: 'کد', key: 'کد',
value: val.id ?? '-' value: val.id ?? '-'
}, },
{
key: 'نوع ثبت',
value: val.customer_id ? 'اختصاصی' : Number(val.show_in_app) === 1 ? 'عمومی — نمایش در اپ' : 'عمومی — مخفی (فقط با کد)'
},
{
key: 'مشتری',
value: giftCardCustomerLabel(val)
},
{
key: 'موبایل مشتری',
value: giftCardCustomerMobile(val)
},
{ {
key: 'نوع کارت هدیه', key: 'نوع کارت هدیه',
value: val.unit === 3 ? 'تخفیف' : val.unit === 2 ? metalLabel.goldWalletShort() : 'ریالی' value: val.unit === 3 ? 'تخفیف' : val.unit === 2 ? metalLabel.goldWalletShort() : 'ریالی'
@@ -261,8 +275,9 @@ async function edit(record: Giftcard) {
addGiftCardVisible.value = true addGiftCardVisible.value = true
editId = record.id editId = record.id
form.value = { form.value = {
mobile: record.customer?.mobile ?? '', assignMode: record.customer_id ? 'customer' : 'public',
name: record.customer?.name ?? '', customer_id: record.customer_id ?? undefined,
show_in_app: record.customer_id ? 0 : Number(record.show_in_app) === 1 ? 1 : 0,
count: record.count, count: record.count,
amount: record.amount, amount: record.amount,
unit: record.unit.toString(), unit: record.unit.toString(),
@@ -330,8 +345,9 @@ async function addGiftOrDiscount(type: 'gift' | 'discount' = 'gift') {
const resetData = () => { const resetData = () => {
giftCardEditorRef.value?.resetFields?.() giftCardEditorRef.value?.resetFields?.()
form.value = { form.value = {
mobile: '', assignMode: 'public',
name: '', customer_id: undefined,
show_in_app: 0,
count: undefined, count: undefined,
amount: undefined, amount: undefined,
unit: '1', unit: '1',
@@ -353,19 +369,26 @@ async function ok() {
await giftCardEditorRef.value?.validateFields?.().then(async () => { await giftCardEditorRef.value?.validateFields?.().then(async () => {
loading.value = true loading.value = true
try { try {
const formData = { const formData: Record<string, unknown> = {
...form.value,
count: Number(form.value.count), count: Number(form.value.count),
amount: Number(form.value.amount), amount: Number(form.value.amount),
unit: form.value.unit,
expiry_date: form.value.expiry_date,
description: form.value.description,
}
if (form.value.assignMode === 'customer' && form.value.customer_id) {
formData.customer_id = Number(form.value.customer_id)
} else if (form.value.assignMode === 'public') {
formData.show_in_app = Number(form.value.show_in_app) === 1 ? 1 : 0
} }
if (formData.expiry_date) { if (formData.expiry_date) {
// تبدیل به میلادی و تنظیم به آخر روز // تبدیل به میلادی و تنظیم به آخر روز
formData.expiry_date = dayjs(formData.expiry_date) formData.expiry_date = dayjs(formData.expiry_date as string)
.calendar('gregory') .calendar('gregory')
.endOf('day') .endOf('day')
.format('YYYY-MM-DD HH:mm:ss') .format('YYYY-MM-DD HH:mm:ss')
} }
await createGiftCard(formData) await createGiftCard(formData as GiftcardInput)
message.success('کارت هدیه با موفقیت ثبت شد') message.success('کارت هدیه با موفقیت ثبت شد')
addGiftCardVisible.value = false addGiftCardVisible.value = false
resetData() resetData()
@@ -0,0 +1,32 @@
import type { Giftcard } from '@/model/giftCard'
export function resolveGiftCardCustomer(giftCard?: Giftcard | null) {
if (!giftCard) return null
const name = giftCard.customer?.name ?? giftCard.name
const mobile = giftCard.customer?.mobile ?? giftCard.mobile
if (name || mobile) {
return {
name: name || '-',
mobile: mobile || '-',
}
}
if (giftCard.customer_id) {
return {
name: `مشتری #${giftCard.customer_id}`,
mobile: '-',
}
}
return null
}
export function giftCardCustomerLabel(giftCard?: Giftcard | null) {
return resolveGiftCardCustomer(giftCard)?.name ?? '— (کد عمومی)'
}
export function giftCardCustomerMobile(giftCard?: Giftcard | null) {
return resolveGiftCardCustomer(giftCard)?.mobile ?? '-'
}
+8 -5
View File
@@ -41,17 +41,20 @@ export const settingsProfileBannersThemeHelp =
export const pricingPageIntro = export const pricingPageIntro =
'اینجا تاریخچهٔ همگام‌سازی قیمت را می‌بینید. دکمهٔ «به‌روزرسانی قیمت» همان لحظه قیمت را از منبعی که در تنظیمات مشخص کرده‌اید می‌گیرد و روی محصولات اعمال می‌کند. اگر قیمت عوض نشده باشد پیام «تغییری نکرد» می‌آید.' 'اینجا تاریخچهٔ همگام‌سازی قیمت را می‌بینید. دکمهٔ «به‌روزرسانی قیمت» همان لحظه قیمت را از منبعی که در تنظیمات مشخص کرده‌اید می‌گیرد و روی محصولات اعمال می‌کند. اگر قیمت عوض نشده باشد پیام «تغییری نکرد» می‌آید.'
export const giftCardMobileHelp = export const giftCardAssignModeHelp =
'شماره موبایلی که کارت هدیه برایش ساخته می‌شود. باید ۱۱ رقم و با ۰۹ شروع شود.' 'اختصاص به مشتری: کدها فقط برای همان شخص در اپ قابل استفاده است. کد عمومی: فقط کد ساخته می‌شود و بعداً به هر کسی می‌توانید بدهید.'
export const giftCardNameHelp = export const giftCardCustomerSelectHelp =
'نام مشتری فقط برای شناسایی در پنل شماست؛ روی اپ مشتری تأثیر مستقیم ندارد.' 'مشتری را از لیست انتخاب کنید؛ نام و موبایل خودکار از پروندهٔ مشتری پر می‌شود.'
export const giftCardPublicVisibilityHelp =
'نمایش برای همه: کد(ها) در لیست اپ همه کاربران دیده می‌شود. مخفی: فقط کسی که کد را از مدیر گرفته و دستی وارد کند می‌تواند استفاده کند.'
export const giftCardUnitHelp = export const giftCardUnitHelp =
'ریالی = اعتبار کیف پول (ریال). مس = وزن مس که به کیف فلزی مشتری واریز می‌شود.' 'ریالی = اعتبار کیف پول (ریال). مس = وزن مس که به کیف فلزی مشتری واریز می‌شود.'
export const giftCardCountHelp = export const giftCardCountHelp =
'چند کد یکتا ساخته شود. مثلاً ۳ → سه کارت هدیه جدا، هر کدام یک کد و یک شارژ کامل با همان مبلغ.' 'تعداد کد یکتا در همان بچ. مثلاً ۳ → یک رکورد با سه کد جداگانه و همان مبلغ برای هر کد.'
export const giftCardAmountHelp = export const giftCardAmountHelp =
'مقدار هر کد: اگر ریالی انتخاب کردید مبلغ به ریال؛ اگر مس انتخاب کردید وزن به واحد معامله.' 'مقدار هر کد: اگر ریالی انتخاب کردید مبلغ به ریال؛ اگر مس انتخاب کردید وزن به واحد معامله.'