Files
boursecu_admin/src/pages/menuPages/customers/base/components/table.vue
T
mohmdsdgandCursor 1c4b2d4435 Require review before Tahesab wallet sync and harden accounting admin UX.
Single-customer and row sync preview balances with documents before overwrite/keep; bulk sync keeps confirm. Also ship Zarin wallet sync actions, field help, and get_user cache improvements.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 21:18:26 +03:30

457 lines
17 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
<BasicShowContext title="لیست مشتریان" :help="customersPageIntro">
<template #extra>
<BaseButton
v-if="isMobile"
type="primary"
size="small"
icon="vuesax-linear:user-add"
@click="emits('add')"
>
افزودن
</BaseButton>
</template>
<div class="flex h-full min-h-0 flex-col">
<BaseTable v-if="!isMobile" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
<template #extraHeader>
<BaseButton type="primary" size="small" icon="vuesax-linear:user-add" @click="emits('add')">
افزودن مشتری
</BaseButton>
</template>
<template #accountingCode="{ record }">
<button
type="button"
class="inline-flex items-center gap-1.5 border-none bg-transparent p-0 text-sm cursor-pointer"
style="color: var(--admin-text)"
@click="openAccountingCodeEdit(record)"
>
<span dir="ltr">{{ accountingCodeOf(record) }}</span>
<Icon icon="vuesax-linear:edit-2" size="16" color="#4572E9" />
</button>
</template>
<template #action="{ record }">
<div class="gap-2 hidden md:flex justify-center">
<a-tooltip :key="record.is_archive" placement="bottom" class="cursor-pointer">
<template #title>
<span>{{ record.is_archive ? 'بیرون بردن از بایگانی' : 'بایگانی کردن' }}</span>
</template>
<a-popconfirm
title="آیا مشتری را بایگانی می کنید؟"
ok-text="تایید"
cancel-text="رد"
placement="topLeft"
@confirm="archiveFunction(record.id)"
>
<Icon class="cursor-pointer" icon="vuesax-linear:archive-tick" color="#e3bf00" />
</a-popconfirm>
</a-tooltip>
<a-tooltip
:key="record.active"
placement="bottom"
class="cursor-pointer"
@click="changeCustomerActivationFunction(record.id)"
>
<template #title>
<span>{{ record.active ? 'غیرفعال کردن حساب کاربری' : 'فعال کردن حساب کاربری' }}</span>
</template>
<Icon
:icon="record.active ? 'vuesax-linear:user-tick' : 'vuesax-linear:user-remove'"
:color="record.active ? '#54AD06' : '#ad0627'"
/>
</a-tooltip>
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('chargeWallet', record)">
<template #title><span>شارژ کیف پول</span></template>
<Icon icon="vuesax-linear:wallet-add" color="#9333ea" />
</a-tooltip>
<a-tooltip
v-if="tahesabEnabled && !isValidAccountingCode(record.moshtari_code)"
placement="bottom"
class="cursor-pointer"
@click="openTahesabRegister(record)"
>
<template #title><span>در ته‌حساب ثبت نشده — کلیک برای ثبت</span></template>
<Icon icon="vuesax-linear:profile-add" color="#CA8A04" />
</a-tooltip>
<a-tooltip
v-if="zarinEnabled && !isValidAccountingCode(record.moshtari_code)"
placement="bottom"
class="cursor-pointer"
@click="openZarinRegister(record)"
>
<template #title><span>در زرین ثبت نشده — کلیک برای ثبت</span></template>
<Icon icon="vuesax-linear:profile-add" color="#0284C7" />
</a-tooltip>
<a-tooltip
v-if="pargarEnabled && !isValidAccountingCode(record.pargar_account_id)"
placement="bottom"
class="cursor-pointer"
@click="openPargarRegister(record)"
>
<template #title><span>در پرگار ثبت نشده — کلیک برای ثبت</span></template>
<Icon icon="vuesax-linear:profile-add" color="#7C3AED" />
</a-tooltip>
<a-tooltip
v-if="accountingEnabled"
placement="bottom"
class="cursor-pointer"
@click="openAccountingCodeEdit(record)"
>
<template #title><span>ویرایش کد حسابداری</span></template>
<Icon icon="vuesax-linear:card-edit" color="#0d9488" />
</a-tooltip>
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)">
<template #title><span>مشخصات</span></template>
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
</a-tooltip>
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('edit', record)">
<template #title><span>ویرایش</span></template>
<Icon icon="vuesax-linear:edit" color="#4572E9" />
</a-tooltip>
<a-tooltip
v-if="customerLevelProfitEnabled"
placement="bottom"
class="cursor-pointer"
@click="openLevelProfit(record)"
>
<template #title><span>سود چندسطحی معرف</span></template>
<Icon icon="vuesax-linear:hierarchy-2" color="#ca8a04" />
</a-tooltip>
<a-tooltip
:key="record.trade_active"
placement="bottom"
class="cursor-pointer"
@click="changeCustomerOrderActivationFunction(record.id)"
>
<template #title>
<span>{{ record.trade_active ? 'غیر مجاز کردن سفارش دهی' : 'مجاز کردن سفارش دهی' }}</span>
</template>
<Icon
:icon="record.trade_active === 1 ? 'vuesax-linear:box-tick' : 'vuesax-linear:box-remove'"
:color="record.trade_active === 1 ? '#54AD06' : '#ad0627'"
/>
</a-tooltip>
<a-tooltip
v-if="customerDirectSmsEnabled"
placement="bottom"
class="cursor-pointer"
@click="openDirectSms(record)"
>
<template #title><span>ارسال پیامک</span></template>
<Icon icon="vuesax-linear:sms" color="#0ea5e9" />
</a-tooltip>
<a-tooltip
v-if="!record.is_legal"
placement="bottom"
class="cursor-pointer"
@click="openConfirmUser(record)"
>
<template #title><span>تایید کاربر</span></template>
<Icon icon="vuesax-linear:user-search" color="#0d9488" />
</a-tooltip>
</div>
</template>
</BaseTable>
<CustomerMobileCardList
v-else
ref="mobileListRef"
class="min-h-0 flex-1"
@info="emits('info', $event)"
@edit="emits('edit', $event)"
@archive="handleArchive"
@toggleActive="(item) => changeCustomerActivationFunction(item.id)"
@chargeWallet="emits('chargeWallet', $event)"
@toggleTrade="(item) => changeCustomerOrderActivationFunction(item.id)"
@confirmUser="openConfirmUser"
@sendSms="openDirectSms"
@editAccounting="openAccountingCodeEdit"
@levelProfit="openLevelProfit"
/>
</div>
<CustomerDirectSmsModal v-model:visible="directSmsVisible" :customer="directSmsCustomer" />
<CustomerLevelProfitModal
v-model:visible="levelProfitVisible"
:customer="levelProfitCustomer"
@success="fetch"
/>
<CustomerAccountingCodeModal
v-model:visible="accountingCodeVisible"
:customer="accountingCodeCustomer"
:provider="accountingProviderSetting"
@success="fetch"
/>
<BasicModal
v-model:visible="confirmVisible"
:width="isMobile ? '92vw' : '50vw'"
title="تایید مشتری"
@ok="confirmUserFunction"
ok-text="تایید"
:loading="loading"
@close="confirmVisible = false"
>
<div class="mt-[-10px] mb-10 font-bold">آیا از اطلاعات وارد شده اطمینان دارید؟</div>
<div class="grid grid-cols-4 gap-2">
<BasicInfoItem title="نام و نام خانوادگی" :value="confirmInfo?.name" />
<BasicInfoItem title="شماره همراه" :value="confirmInfo?.mobile" />
<BasicInfoItem
title="تاریخ تولد"
:value="confirmInfo?.birth_date ? toJalaliNoTime(confirmInfo?.birth_date) : '_'"
/>
<BasicInfoItem title="شماره تلفن" :value="confirmInfo?.phone_number ?? '_'" />
<BasicInfoItem title="نام استان" :value="confirmInfo?.province" />
<BasicInfoItem title="نام شهر" :value="confirmInfo?.city" />
<BasicInfoItem title="شماره ملی" :value="confirmInfo?.n_code" />
</div>
</BasicModal>
<TahesabRetryModal
v-if="tahesabEnabled"
v-model="tahesabRegisterVisible"
mode="register-customer"
:customer-id="tahesabCustomer?.id"
:customer-name="tahesabCustomer?.name"
@success="fetch"
/>
<ZarinRetryModal
v-if="zarinEnabled"
v-model="zarinRegisterVisible"
mode="register-customer"
:customer-id="zarinCustomer?.id"
:customer-name="zarinCustomer?.name"
@success="fetch"
/>
<PargarRetryModal
v-if="pargarEnabled"
v-model="pargarRegisterVisible"
mode="register-customer"
:customer-id="pargarCustomer?.id"
:customer-name="pargarCustomer?.name"
@success="fetch"
/>
</BasicShowContext>
</div>
</template>
<script setup lang="ts">
import { message, Modal } from 'ant-design-vue'
import { onMounted, ref, computed, watch } from 'vue'
import {
changeCustomerActivation,
changeCustomerArchive,
changeCustomerOrderActivation,
confirmUser,
getCustomerTable
} from '@/api/customer'
import { buildCustomerColumns } from '@/pages/menuPages/customers/base/hooks/customerColumns'
import CustomerMobileCardList from '@/pages/menuPages/customers/base/components/CustomerMobileCardList.vue'
import CustomerDirectSmsModal from '@/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue'
import CustomerLevelProfitModal from '@/pages/menuPages/customers/base/components/CustomerLevelProfitModal.vue'
import CustomerAccountingCodeModal from '@/pages/menuPages/customers/base/components/CustomerAccountingCodeModal.vue'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
import type { TableInput } from '@core/type/baseTable'
import { toJalaliNoTime } from '@core/utils/useDataTime'
import type { Customer } from '@/model/customer'
import { MODAL_ABOVE_ACTION_SHEET_Z_INDEX } from '@/constants/overlayZIndex'
import { isMobile } from '@core/utils/is'
import TahesabRetryModal from '@/components/TahesabRetryModal.vue'
import ZarinRetryModal from '@/components/ZarinRetryModal.vue'
import PargarRetryModal from '@/components/PargarRetryModal.vue'
import {
tahesabEnabledSetting,
zarinEnabledSetting,
pargarEnabledSetting,
accountingProviderSetting,
customerDirectSmsEnabledSetting,
customerLevelProfitEnabledSetting,
initAccountingSettingsFromApi,
initWalletSettingsFromApi
} from '@/utils/config'
import { displayAccountingCode, isValidAccountingCode } from '@/utils/accountingCode'
import { customersPageIntro } from '@/utils/adminHelpCopy'
const [submit, reload] = useBaseTable()
const emits = defineEmits(['delete', 'edit', 'add', 'info', 'chargeWallet'])
const loading = ref(false)
const confirmVisible = ref(false)
const confirmInfo = ref<Customer>()
const tahesabRegisterVisible = ref(false)
const tahesabCustomer = ref<Customer>()
const tahesabEnabled = tahesabEnabledSetting
const zarinRegisterVisible = ref(false)
const zarinCustomer = ref<Customer>()
const zarinEnabled = zarinEnabledSetting
const pargarRegisterVisible = ref(false)
const pargarCustomer = ref<Customer>()
const pargarEnabled = pargarEnabledSetting
const customerDirectSmsEnabled = customerDirectSmsEnabledSetting
const customerLevelProfitEnabled = customerLevelProfitEnabledSetting
const levelProfitVisible = ref(false)
const levelProfitCustomer = ref<Customer | null>(null)
const accountingCodeVisible = ref(false)
const accountingCodeCustomer = ref<Customer | null>(null)
const accountingEnabled = computed(() => {
const provider = accountingProviderSetting.value
return provider === 'pargar' || provider === 'tahesab' || provider === 'zarin'
})
function accountingCodeOf(record: Customer) {
const provider = accountingProviderSetting.value
if (provider === 'pargar') return displayAccountingCode(record.pargar_account_id)
if (provider === 'tahesab' || provider === 'zarin') return displayAccountingCode(record.moshtari_code)
return '-'
}
function openAccountingCodeEdit(user: Customer) {
accountingCodeCustomer.value = user
accountingCodeVisible.value = true
}
const directSmsVisible = ref(false)
const directSmsCustomer = ref<Customer | null>(null)
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
const catalogMetals = ref<Parameters<typeof buildCustomerColumns>[0]>([])
function rebuildColumns(metals = catalogMetals.value) {
catalogMetals.value = metals
tblProps.value.columns = buildCustomerColumns(metals, {
accountingProvider: accountingProviderSetting.value
})
}
watch(accountingProviderSetting, () => {
rebuildColumns()
})
const tblProps = ref<any>({
columns: buildCustomerColumns([], { accountingProvider: accountingProviderSetting.value }),
api: async (params: TableInput) => {
await initAccountingSettingsFromApi()
rebuildColumns(catalogMetals.value)
const data = await getCustomerTable(params)
const metals = data?.catalogMetals ?? []
rebuildColumns(metals)
return data
}
})
const fetch = async () => {
await Promise.allSettled([reload(), mobileListRef.value?.fetch?.() as Promise<void>])
}
defineExpose({ fetch })
onMounted(() => {
void (async () => {
await initAccountingSettingsFromApi()
rebuildColumns()
await initWalletSettingsFromApi()
})()
})
function openDirectSms(user: Customer) {
directSmsCustomer.value = user
directSmsVisible.value = true
}
function openLevelProfit(user: Customer) {
levelProfitCustomer.value = user
levelProfitVisible.value = true
}
async function changeCustomerActivationFunction(id: number) {
try {
loading.value = true
await changeCustomerActivation(id)
await fetch()
message.success('تغییر وضعیت با موفقیت انجام شد')
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
function openConfirmUser(user: Customer) {
confirmInfo.value = user
confirmVisible.value = true
}
function openTahesabRegister(user: Customer) {
tahesabCustomer.value = user
tahesabRegisterVisible.value = true
}
function openZarinRegister(user: Customer) {
zarinCustomer.value = user
zarinRegisterVisible.value = true
}
function openPargarRegister(user: Customer) {
pargarCustomer.value = user
pargarRegisterVisible.value = true
}
function handleArchive(customer: Customer) {
Modal.confirm({
title: 'آیا مشتری را بایگانی می کنید؟',
okText: 'تایید',
cancelText: 'رد',
centered: true,
zIndex: MODAL_ABOVE_ACTION_SHEET_Z_INDEX,
onOk: () => archiveFunction(customer.id)
})
}
async function archiveFunction(id: number) {
try {
loading.value = true
await changeCustomerArchive(id)
await fetch()
message.success('مشتری با موفقیت بایگانی شد.')
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
async function confirmUserFunction() {
try {
loading.value = true
await confirmUser(confirmInfo.value?.id)
await fetch()
confirmVisible.value = false
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
async function changeCustomerOrderActivationFunction(id: number) {
try {
loading.value = true
await changeCustomerOrderActivation(id)
await fetch()
message.success('تغییر وضعیت سفارش دهی با موفقیت انجام شد')
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
</script>
<style lang="less">
.ant-popover,
.ant-tooltip {
z-index: 1001 !important;
}
</style>