fix bug of pargar 3

This commit is contained in:
2026-07-26 19:17:05 +03:30
parent 017e375c4d
commit 14837f70a9
9 changed files with 352 additions and 36 deletions
+3
View File
@@ -7,12 +7,15 @@ export interface PargarRecord {
amount?: number
desc?: string
type?: string
order_id?: number | null
withdraw_id?: number | null
sync_status?: 'pending' | 'success' | 'failed'
sync_log?: string | null
pargar_ref?: string | null
data?: string | null
res?: string | null
created_at?: string
confirmed_at?: string | null
customer?: {
id?: number
name?: string
@@ -21,7 +21,7 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
title: 'دسترسی مشتری به قابلیت‌ها',
icon: 'vuesax-linear:people',
match: (k) =>
/^(banner_enabled|tracking_systems_enabled|jibit_verification|register_jibit_verify)$/.test(k)
/^(banner_enabled|tracking_systems_enabled|jibit_verification|register_jibit_verify|show_product_details)$/.test(k)
},
{
id: 'trading_hours',
@@ -268,6 +268,7 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
show_last_price: 'switch',
show_home_price_when_closed: 'switch',
show_gold_ounce_price: 'switch',
show_product_details: 'switch',
send_to_tahesab: 'switch',
accounting_provider: 'select',
profit_gold_api_enabled: 'switch',
@@ -561,6 +562,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
'روشن: وقتی فروشگاه خارج از ساعت کاری است، قیمت صفحه اصلی اپ همچنان نمایش داده می‌شود و پیام «فروشگاه بسته است» نشان داده نمی‌شود. خرید/فروش همچنان بسته می‌ماند. خاموش (پیش‌فرض): خارج از ساعت پیام بسته بودن نشان داده می‌شود.',
show_gold_ounce_price:
'روشن: اگر آدرس API تابلو (zaarbox_api_url) تنظیم شده باشد، قیمت انس طلا در اپ مشتری نمایش داده می‌شود. خاموش: نمایش داده نمی‌شود.',
show_product_details:
'روشن: بعد از انتخاب محصول در صفحه خرید/فروش، متن توضیح محصول نمایش داده می‌شود. خاموش: توضیح نشان داده نمی‌شود.',
fee_single:
'اختلاف پایه (ریال) بین قیمت خرید و فروش روی مظنه. معمولاً نصف مجموع اختلاف قیمت خرید و فروش است.',
buy_fee:
@@ -819,6 +822,7 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
show_last_price: 'نمایش آخرین قیمت ذخیره‌شده',
show_home_price_when_closed: 'نمایش قیمت صفحه اصلی خارج از ساعت کاری',
show_gold_ounce_price: 'نمایش قیمت انس طلا',
show_product_details: 'نمایش جزئیات اطلاعات محصول',
fee_single: 'اختلاف پایه خرید/فروش',
buy_fee: 'اضافه قیمت خرید',
sell_fee: 'کسر قیمت فروش',
@@ -978,7 +982,7 @@ export const SETTING_EDITABLE_KEYS = new Set([
'open_register',
'referral_id', 'referral_limit', 'referral_gift_on_first_order',
'register_jibit_verify', 'register_referral_bonus_message', 'register_referral_sms_to_referrer',
'sell_fee', 'show_last_price', 'show_home_price_when_closed', 'show_gold_ounce_price', 'startDay',
'sell_fee', 'show_last_price', 'show_home_price_when_closed', 'show_gold_ounce_price', 'show_product_details', 'startDay',
'support_messengers', 'support_messenger_text',
'deposit_support_message_text', 'deposit_account_holder', 'deposit_card_number', 'deposit_sheba',
'text_about', 'text_rule',
@@ -0,0 +1,265 @@
<template>
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
<BasicShowContext title="لاگ‌ها و اسناد پرگار">
<div class="min-w-0 w-full max-w-full overflow-x-auto">
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
<template #action="{ record }">
<div class="flex justify-center gap-2">
<a-tooltip placement="bottom" class="cursor-pointer" @click="openInfo(record)">
<template #title><span>اطلاعات سند</span></template>
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
</a-tooltip>
<a-tooltip
v-if="canRetryPargar(record)"
placement="bottom"
class="cursor-pointer"
@click="openSend(record)"
>
<template #title><span>ارسال به پرگار</span></template>
<Icon icon="vuesax-linear:send-2" color="#CA8A04" />
</a-tooltip>
</div>
</template>
</BaseTable>
</div>
</BasicShowContext>
<BasicModal v-model:visible="infoVisible" title="اطلاعات سند پرگار" width="36rem" :footer="null">
<div class="space-y-2 text-sm">
<div
v-for="item in infoItems"
:key="item.label"
class="flex justify-between gap-3 border-b border-gray-100 py-2"
>
<span class="text-gray-500">{{ item.label }}</span>
<span class="break-all text-left">{{ item.value ?? '-' }}</span>
</div>
<div
v-if="selected?.res"
class="rounded-lg bg-gray-100 p-3 text-xs font-mono text-gray-700 whitespace-pre-wrap"
>
{{ selected.res }}
</div>
<div v-if="parsedPayload?.transactions.length" class="space-y-2">
<div class="text-sm font-600 text-gray-700">جزئیات اسناد دریافتی از پرگار</div>
<div
v-for="(tx, idx) in parsedPayload.transactions"
:key="idx"
class="rounded-lg border border-gray-100 bg-gray-50 p-3 text-xs space-y-1"
>
<div class="flex justify-between gap-2">
<span class="text-gray-500">شرح</span>
<span class="text-left">{{ tx.desc || tx.docType || '-' }}</span>
</div>
<div class="flex justify-between gap-2">
<span class="text-gray-500">تاریخ</span>
<span class="text-left">{{ tx.date || '-' }}</span>
</div>
<div class="flex justify-between gap-2">
<span class="text-gray-500">شماره سند</span>
<span class="text-left">{{ tx.docId || '-' }}</span>
</div>
<div class="flex justify-between gap-2">
<span class="text-gray-500">مبالغ</span>
<span class="text-left">{{ formatTransactionAmount(tx) }}</span>
</div>
<div v-if="tx.remainGold || tx.remainRial" class="flex justify-between gap-2">
<span class="text-gray-500">مانده</span>
<span class="text-left">
{{ tx.remainGold ? `${tx.remainGold} گرم` : '' }}
{{ tx.remainRial ? `${tx.remainRial} ریال` : '' }}
</span>
</div>
</div>
</div>
<div
v-else-if="selected?.data"
class="rounded-lg bg-gray-100 p-3 text-xs font-mono text-gray-700 whitespace-pre-wrap overflow-x-auto"
>
{{ selected.data }}
</div>
<div v-if="selected?.sync_log" class="rounded-lg bg-red-50 p-3 text-red-700 whitespace-pre-wrap">
{{ selected.sync_log }}
</div>
</div>
</BasicModal>
<PargarRetryModal
v-model="sendVisible"
mode="retry-sanad"
:pargar-id="selected?.id"
@success="fetch"
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
import type { TableInput } from '@core/type/baseTable'
import { getPargarTable, type PargarRecord } from '@/api/pargar'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import BasicModal from '@/components/BasicModal.vue'
import PargarRetryModal from '@/components/PargarRetryModal.vue'
import { canRetryPargar, pargarStatusLabel } from '@/pages/menuPages/accounting/utils/pargarStatus'
import {
formatTransactionAmount,
parsePargarPayload,
pargarAmountLabel,
pargarDescLabel,
pargarTypeLabel
} from '@/pages/menuPages/accounting/utils/pargarDisplay'
import { toJalali } from '@core/utils/useDataTime'
const props = defineProps<{
customerId?: number
}>()
const [submit, reload] = useBaseTable()
const infoVisible = ref(false)
const sendVisible = ref(false)
const selected = ref<PargarRecord | null>(null)
const columns = [
{
title: 'ردیف',
dataIndex: 'id',
key: 'id',
filterable: false,
sortable: false,
customRender: (r: any) => r.index + 1
},
{
title: 'شناسه',
dataIndex: 'id',
key: 'log_id',
type: 'Number',
filterable: false
},
{
title: 'نوع',
dataIndex: 'type',
key: 'type',
type: 'Text',
customRender: (r: any) => pargarTypeLabel(r.record.type)
},
{
title: 'مقدار',
dataIndex: 'amount',
key: 'amount',
type: 'Number',
customRender: (r: any) => pargarAmountLabel(r.record)
},
{
title: 'شرح',
dataIndex: 'desc',
key: 'desc',
type: 'Text',
customRender: (r: any) => pargarDescLabel(r.record)
},
{
title: 'سفارش',
dataIndex: 'order_id',
key: 'order_id',
type: 'Number',
filterable: false,
customRender: (r: any) => r.record.order_id || '-'
},
{
title: 'برداشت',
dataIndex: 'withdraw_id',
key: 'withdraw_id',
type: 'Number',
filterable: false,
customRender: (r: any) => r.record.withdraw_id || '-'
},
{
title: 'وضعیت',
dataIndex: 'sync_status',
key: 'sync_status',
type: 'Text',
filterable: false,
customRender: (r: any) =>
pargarStatusLabel(r.record.sync_status, r.record.pargar_ref, r.record.desc)
},
{
title: 'زمان',
dataIndex: 'created_at',
key: 'created_at',
type: 'Text',
customRender: (r: any) => toJalali(r.record.created_at)
},
{
title: 'عملیات',
dataIndex: 'action',
key: 'action',
slot: 'action',
filterable: false
}
]
const tblProps = ref({
columns,
api: (params: TableInput) => {
if (props.customerId) {
params.filters = []
params.filters.push({
field: 'customer_id',
type: 'text',
value: props.customerId,
cr: 'and',
op: 'eq',
hidden: false
})
}
return getPargarTable(params)
}
})
const infoItems = computed(() => {
const row = selected.value
if (!row) return []
const payload = parsePargarPayload(row.data)
return [
{ label: 'شناسه سند', value: row.id },
{ label: 'کد حساب پرگار', value: payload?.accountId || row.pargar_ref || '-' },
{ label: 'نوع', value: pargarTypeLabel(row.type) },
{ label: 'مقدار', value: pargarAmountLabel(row) },
{ label: 'شرح', value: pargarDescLabel(row) },
{ label: 'سفارش', value: row.order_id || '-' },
{ label: 'برداشت', value: row.withdraw_id || '-' },
{
label: 'تعداد سطر سند',
value: payload?.transactions.length ? String(payload.transactions.length) : '-'
},
{ label: 'زمان ثبت', value: row.created_at ? toJalali(row.created_at) : '-' },
{
label: 'تأیید پرگار',
value: row.confirmed_at ? toJalali(row.confirmed_at) : '-'
},
{
label: 'وضعیت',
value: pargarStatusLabel(row.sync_status, row.pargar_ref, row.desc)
},
{ label: 'کد پرگار', value: row.pargar_ref || 'ثبت نشده' }
]
})
const parsedPayload = computed(() => parsePargarPayload(selected.value?.data))
function openInfo(record: PargarRecord) {
selected.value = record
infoVisible.value = true
}
function openSend(record: PargarRecord) {
selected.value = record
sendVisible.value = true
}
async function fetch() {
await reload()
}
defineExpose({ fetch })
</script>
@@ -26,38 +26,49 @@
>
همگام‌سازی مانده از پرگار
</a-button>
<a-popconfirm
title="ماندهٔ پرگار بدون بررسی اعتبار روی کیف مشتری نوشته شود؟"
ok-text="بله، اعمال کن"
cancel-text="انصراف"
<a-button
type="link"
size="small"
block
danger
class="pargar-wallet-sync__force"
:disabled="!canSync || loading"
@confirm="$emit('sync', true)"
@click="confirmForceSync"
>
<a-button
type="link"
size="small"
block
danger
class="pargar-wallet-sync__force"
:disabled="!canSync || loading"
>
اعمال اجباری مانده
</a-button>
</a-popconfirm>
اعمال اجباری مانده
</a-button>
</div>
</template>
<script setup lang="ts">
import { Modal } from 'ant-design-vue'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { SELECT_DROPDOWN_Z_INDEX } from '@/constants/overlayZIndex'
defineProps<{
const props = defineProps<{
loading?: boolean
canSync?: boolean
}>()
defineEmits<{
const emit = defineEmits<{
(e: 'sync', force: boolean): void
}>()
function confirmForceSync() {
if (!props.canSync || props.loading) return
// Popconfirm داخل مودال جزئیات مشتری پشت می‌افتد؛ Modal.confirm با zIndex درست
Modal.confirm({
title: 'اعمال اجباری مانده',
content: 'ماندهٔ پرگار بدون بررسی اعتبار روی کیف مشتری نوشته شود؟',
okText: 'بله، اعمال کن',
cancelText: 'انصراف',
okType: 'danger',
centered: true,
zIndex: SELECT_DROPDOWN_Z_INDEX,
onOk: () => emit('sync', true)
})
}
</script>
<style scoped>
@@ -34,6 +34,7 @@
:title="modalTitle"
:ok-text="okText"
:loading="submitting"
:confirm-loading="submitting"
@ok="submit"
@close="handleClose"
>
@@ -140,31 +141,34 @@ function closeModal() {
}
async function submit() {
const customerId = formCharge.value.customer_id ?? props.customerId
if (!customerId) {
message.warning('مشتری را انتخاب کنید')
return
}
try {
await bodyRef.value?.validateFields?.()
} catch {
return
}
const target = selectedTarget.value
// قفل همین‌جا بسته می‌شود تا اعتبارسنجیِ async بین دو کلیک، سند تکراری نسازد
if (submitting.value) return
submitting.value = true
try {
const customerId = formCharge.value.customer_id ?? props.customerId
if (!customerId) {
message.warning('مشتری را انتخاب کنید')
return
}
try {
await bodyRef.value?.validateFields?.()
} catch {
return
}
const target = selectedTarget.value
await chargeWalletUnified({
...buildChargePayload({ ...formCharge.value, customer_id: customerId }, target?.product_id ?? null),
})
message.success(walletChargeSuccessMessage(formCharge.value.direction))
emit('success')
closeModal()
} catch (e) {
} catch (e: any) {
console.error(e)
message.error('خطا در ثبت عملیات')
message.error(e?.response?.data?.message ?? 'خطا در ثبت عملیات')
} finally {
submitting.value = false
}
@@ -243,6 +243,9 @@
<a-tab-pane key="قلک ها" tab="قلک ها">
<FundsTable ref="fundTable" :customerId="userInfo?.id" />
</a-tab-pane>
<a-tab-pane v-if="pargarWalletSyncVisible" key="لاگ پرگار" tab="لاگ پرگار">
<CustomerPargarLogsTable ref="pargarLogsTable" :customer-id="userInfo?.id" />
</a-tab-pane>
<a-tab-pane
v-if="conditionalOrderEnabled"
key="سفارش شرطی"
@@ -302,6 +305,7 @@ import transactionsTbl from '@/pages/menuPages/transactions/components/table.vue
import smsTbl from '@/pages/menuPages/sms/components/table.vue'
import FundsTable from '@/pages/menuPages/customers/base/components/fundsTable.vue'
import ReferralsTable from '@/pages/menuPages/customers/base/components/referralsTable.vue'
import CustomerPargarLogsTable from '@/pages/menuPages/customers/base/components/CustomerPargarLogsTable.vue'
import CustomerConditionalOrderTab from '@/pages/menuPages/customers/base/components/CustomerConditionalOrderTab.vue'
import type { Order } from '@/model/order'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
@@ -374,12 +378,14 @@ const transactionTable = ref(null)
const smsTable = ref(null)
const referralTable = ref(null)
const fundTable = ref(null)
const pargarLogsTable = ref<{ fetch?: () => void } | null>(null)
const refreshTable = () => table?.value?.fetch()
const refreshOrderTable = () => orderTable?.value?.fetch()
const refreshTransactionTable = () => transactionTable?.value?.fetch()
const refreshSMSTable = () => smsTable?.value?.fetch()
const refreshReferralTable = () => referralTable?.value?.fetch()
const refreshFundTable = () => fundTable?.value?.fetch()
const refreshPargarLogsTable = () => pargarLogsTable.value?.fetch?.()
let updateKey = ref<number>(0)
@@ -500,6 +506,7 @@ async function info(val: Customer) {
await nextTick(() => {
loading.value = true
refreshFundTable()
refreshPargarLogsTable()
refreshReferralTable()
refreshSMSTable()
refreshTransactionTable()
@@ -678,6 +685,7 @@ async function syncPargarWallet(force = false) {
await refreshWalletPanels()
refreshTransactionTable()
refreshPargarLogsTable()
refreshTable()
} catch (e: unknown) {
// فقط خطای واقعی شبکه/سرور — پیام ۴۲۲ را interceptor هم نشان می‌دهد؛ دوباره نزن
@@ -691,6 +699,7 @@ async function syncPargarWallet(force = false) {
await refreshWalletPanels()
refreshTransactionTable()
refreshPargarLogsTable()
refreshTable()
if (applied) {
@@ -212,6 +212,19 @@
</a-form-item>
</div>
<a-form-item name="description" class="!mb-0">
<template #label>
<DashboardLabelWithHelp label="توضیح محصول" :help="productDescriptionHelp" size="sm" />
</template>
<a-textarea
v-model:value="form.description"
:rows="3"
placeholder="متن توضیح برای نمایش در اپ مشتری (اختیاری)"
:maxlength="2000"
show-count
/>
</a-form-item>
<div v-if="!isMeltedGroup" class="product-editor-panel flex flex-col gap-4">
<div class="product-spec-switch-row">
<div class="product-spec-label">
@@ -523,6 +536,11 @@ const titleHelp: DashboardHelp = {
description: 'نام نمایشی محصول در اپ کاربر و پنل.',
}
const productDescriptionHelp: DashboardHelp = {
description:
'متن توضیح محصول که در اپ مشتری، بعد از انتخاب محصول در صفحه خرید/فروش نمایش داده می‌شود (اگر سوییچ «نمایش جزئیات اطلاعات محصول» در تنظیمات روشن باشد).',
}
const categoryHelp: DashboardHelp = {
description: 'دسته‌بندی محصول در تب‌های اپ (سکه، شمش، آبشده و …).',
}
@@ -420,6 +420,7 @@ function resetData() {
has_physical_delivery: false,
min_physical_delivery_quantity: undefined,
inventory: 0,
description: '',
min_order_cash: undefined,
max_order_cash: undefined,
auto_open_at: dayjs('00:00:00', 'HH:mm:ss'),
@@ -440,6 +441,7 @@ const PRICING_FIELDS = new Set([
])
const DETAILS_FIELDS = new Set([
'title',
'description',
'group_type',
'metal_id',
'amount',