fix some bugs from zarin

This commit is contained in:
2026-07-28 15:04:04 +03:30
parent b1685b47d5
commit 2cb7c605e6
3 changed files with 136 additions and 20 deletions
+19 -3
View File
@@ -54,8 +54,14 @@ export interface CustomerSyncStatus {
panel_total: number
panel_registered: number
panel_only: number
zarin_total: number
zarin_only_count: number
zarin_total: number | null
zarin_only_count: number | null
/** اگر دریافت لیست شکست بخورد */
zarin_fetch_error?: string | null
zarin_http_status?: number | null
/** نام گروه فیلتر (مثلاً مشتری نقدی) */
zarin_group_filter?: string | null
include_zarin?: boolean
panel_only_list: {
data: PanelOnlyCustomer[]
current_page: number
@@ -75,7 +81,17 @@ export const getZarinCustomerSyncStatus = async (params?: {
page?: number
zarin_page?: number
include_zarin?: number | boolean
}) => await useAxios.get<CustomerSyncStatus>('/admin/zarin/customer-sync/status', { params })
}) => {
// روی بعضی سرورها query به PHP نمی‌رسد — لیست زرین از مسیر جدا
if (params?.include_zarin) {
const { include_zarin: _ignored, ...rest } = params
return await useAxios.get<CustomerSyncStatus>(
'/admin/zarin/customer-sync/status-with-zarin',
Object.keys(rest).length ? { params: rest } : undefined,
)
}
return await useAxios.get<CustomerSyncStatus>('/admin/zarin/customer-sync/status', { params })
}
export interface SyncPanelResult {
ok: boolean
@@ -18,11 +18,11 @@
<Icon icon="vuesax-linear:arrow-left" :size="16" />
<Icon icon="vuesax-linear:arrow-right" :size="16" />
</div>
<div class="cs-stat cs-stat--zarin">
<div class="cs-stat cs-stat--zarin cs-stat--clickable" role="button" tabindex="0" @click="onTabChange('zarin')" @keydown.enter="onTabChange('zarin')">
<div class="cs-stat__num">
<template v-if="status?.zarin_total != null">{{ status.zarin_total }}</template>
<a-spin v-else-if="loadingZarin" size="small" />
<span v-else class="cs-stat__hint">تب دوم</span>
<span v-else class="cs-stat__hint">کلیک کنید</span>
</div>
<div class="cs-stat__label">مشتریان زرین</div>
</div>
@@ -38,11 +38,11 @@
</div>
<div class="cs-stat__label">پنل — بدون زرین</div>
</div>
<div class="cs-stat cs-stat--sky">
<div class="cs-stat cs-stat--sky cs-stat--clickable" role="button" tabindex="0" @click="onTabChange('zarin')" @keydown.enter="onTabChange('zarin')">
<div class="cs-stat__num">
<template v-if="status?.zarin_only_count != null">{{ status.zarin_only_count }}</template>
<a-spin v-else-if="loadingZarin" size="small" />
<span v-else class="cs-stat__hint">تب دوم</span>
<span v-else class="cs-stat__hint">کلیک کنید</span>
</div>
<div class="cs-stat__label">زرین — بدون پنل</div>
</div>
@@ -77,6 +77,7 @@
<Icon icon="vuesax-linear:arrow-right" :size="14" />
زرین ← پنل
<span v-if="status?.zarin_only_count" class="cs-tab__badge cs-tab__badge--sky">{{ status.zarin_only_count }}</span>
<span v-else-if="!zarinFetched && !loadingZarin" class="cs-tab__hint">بارگذاری</span>
</button>
</div>
@@ -193,6 +194,10 @@
<span class="cs-card__head-title">مشتریان زرین بدون حساب پنل</span>
</div>
<div class="cs-card__head-actions">
<a-button size="small" :loading="loadingZarin" @click="reloadZarin">
<template #icon><Icon icon="vuesax-linear:refresh" :size="13" /></template>
بروزرسانی از زرین
</a-button>
<a-button
type="primary"
size="small"
@@ -214,6 +219,18 @@
</div>
</div>
<div v-if="status?.zarin_group_filter && status.include_zarin" class="cs-group-filter">
گروه: <b>{{ status.zarin_group_filter }}</b>
</div>
<div v-if="zarinLoadFailed" class="cs-fetch-error">
<Icon icon="vuesax-linear:danger" :size="16" />
<div>
<div class="cs-fetch-error__title">دریافت لیست از زرین ممکن نشد</div>
<div class="cs-fetch-error__msg">اتصال را بررسی کنید و «بروزرسانی از زرین» را دوباره بزنید.</div>
</div>
</div>
<div v-if="syncingZarin || zarinSyncResult" class="cs-result">
<template v-if="syncingZarin">
<a-spin size="small" />
@@ -233,6 +250,12 @@
</div>
<div v-if="loadingZarin" class="cs-loading"><a-spin /></div>
<template v-else-if="zarinLoadFailed">
<div class="cs-empty cs-empty--error">
<div class="cs-empty__title">لیست مشتریان فعلاً در دسترس نیست</div>
<div class="cs-empty__sub">دکمه «بروزرسانی از زرین» را دوباره بزنید.</div>
</div>
</template>
<template v-else-if="zarinOnlyList.length">
<!-- موبایل -->
<div class="cs-mobile-list">
@@ -300,7 +323,10 @@
</template>
<div v-else class="cs-empty">
<div class="cs-empty__icon"><Icon icon="vuesax-linear:tick-circle" :size="28" /></div>
<div class="cs-empty__title">همه مشتریان زرین در پنل ثبت شده‌اند</div>
<div class="cs-empty__title">
<template v-if="(status?.zarin_total ?? 0) > 0">همه مشتریان زرین در پنل ثبت شده‌اند</template>
<template v-else>هنوز لیست زرین بارگذاری نشده یا خالی است</template>
</div>
</div>
</div>
@@ -329,6 +355,7 @@ const importStore = useCustomerImportStore()
// ─── state ───
const loadingPanel = ref(true) // شروع با true تا اولین load کامل بشه
const loadingZarin = ref(false)
const zarinLoadFailed = ref(false)
const syncingPanel = ref(false)
const syncingZarin = ref(false)
const activeTab = ref<'panel' | 'zarin'>('panel')
@@ -404,21 +431,51 @@ async function load(panelPage = 1) {
}
// ─── load zarin customers (کند — فقط وقتی تب زرین باز میشه) ───
async function loadZarin() {
if (zarinFetched.value) return
async function loadZarin(force = false) {
if (zarinFetched.value && !force) return
loadingZarin.value = true
zarinLoadFailed.value = false
try {
const res = await getZarinCustomerSyncStatus({ include_zarin: 1 } as any)
const res = await getZarinCustomerSyncStatus({ include_zarin: 1 })
if (status.value) {
status.value.zarin_total = res.zarin_total ?? 0
status.value.zarin_total = res.zarin_total ?? 0
status.value.zarin_only_count = res.zarin_only_count ?? 0
status.value.zarin_group_filter = res.zarin_group_filter ?? null
status.value.include_zarin = res.include_zarin ?? false
status.value.zarin_fetch_error = null
} else {
status.value = res
status.value = { ...res, zarin_fetch_error: null }
}
zarinOnlyList.value = res.zarin_only_list?.data ?? []
zarinFetched.value = true
} catch (e) { console.error(e) }
finally { loadingZarin.value = false }
if (res.zarin_fetch_error || res.include_zarin === false) {
zarinLoadFailed.value = true
if (status.value) {
status.value.zarin_total = 0
status.value.zarin_only_count = 0
}
message.error('دریافت لیست مشتریان از حسابداری ناموفق بود')
}
} catch (e) {
console.error(e)
zarinLoadFailed.value = true
zarinFetched.value = true
if (status.value) {
status.value.zarin_total = 0
status.value.zarin_only_count = 0
}
message.error('دریافت لیست مشتریان از حسابداری ناموفق بود')
} finally {
loadingZarin.value = false
}
}
function reloadZarin() {
zarinFetched.value = false
zarinLoadFailed.value = false
selectedZarin.clear()
zarinPage.value = 1
loadZarin(true)
}
function onTabChange(tab: 'panel' | 'zarin') {
@@ -609,6 +666,11 @@ onMounted(() => {
&--zarin { background: rgba(14,165,233,0.25); }
&--amber { background: rgba(251,191,36,0.2); }
&--sky { background: rgba(14,165,233,0.2); }
&--clickable {
cursor: pointer;
transition: background 0.15s ease;
&:hover { background: rgba(14,165,233,0.4); }
}
&__num {
font-size: 20px;
@@ -702,6 +764,15 @@ onMounted(() => {
&--amber { background: rgba(245,158,11,0.15); color: #d97706; }
&--sky { background: rgba(14,165,233,0.15); color: #0ea5e9; }
}
&__hint {
font-size: 10px;
font-weight: 600;
padding: 1px 6px;
border-radius: 99px;
background: rgba(14,165,233,0.12);
color: #0ea5e9;
}
}
/* ─── کارت ─── */
@@ -786,6 +857,33 @@ onMounted(() => {
.cs-loading { display: flex; justify-content: center; padding: 48px; }
.cs-group-filter {
padding: 8px 16px;
font-size: 12px;
color: var(--admin-text-secondary, #64748b);
border-bottom: 1px solid var(--admin-border);
background: var(--admin-bg-muted, transparent);
b { color: var(--admin-text); }
&__warn { color: #dc2626; font-weight: 600; }
}
.cs-fetch-error {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 16px;
border-bottom: 1px solid rgba(239, 68, 68, 0.25);
background: rgba(239, 68, 68, 0.08);
color: #dc2626;
font-size: 12px;
line-height: 1.6;
&__title { font-weight: 700; font-size: 13px; margin-bottom: 2px; }
&__msg { word-break: break-word; color: var(--admin-text); }
&__meta { margin-top: 4px; opacity: 0.8; font-family: ui-monospace, monospace; }
}
/* ─── موبایل ─── */
.cs-mobile-list {
display: block;
@@ -852,6 +950,8 @@ onMounted(() => {
&__icon { width: 52px; height: 52px; border-radius: 50%; background: rgba(52,211,153,0.12); color: #059669; display: flex; align-items: center; justify-content: center; }
&__title { font-size: 14px; font-weight: 700; color: var(--admin-text); }
&__sub { font-size: 12px; color: var(--admin-text-muted, #64748b); text-align: center; max-width: 360px; }
&--error &__title { color: #dc2626; }
}
.cs-pagination {
@@ -179,6 +179,7 @@ export function supportMessengerIdPlaceholder(messenger?: string): string {
export const HIDDEN_LEGACY_SETTING_KEYS = new Set([
'main_product_price',
'montzar_price',
'fee_single',
'etehadie_api_url',
'goldir_price',
'price_primary_source',
@@ -566,11 +567,11 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
show_product_details:
'روشن: بعد از انتخاب محصول در صفحه خرید/فروش، متن توضیح محصول نمایش داده می‌شود. خاموش: توضیح نشان داده نمی‌شود.',
fee_single:
'اختلاف پایه (ریال) بین قیمت خرید و فروش روی مظنه. معمولاً نصف مجموع اختلاف قیمت خرید و فروش است.',
'منسوخ — دیگر استفاده نمی‌شود. اختلاف فقط از buy_fee / sell_fee (خالی = صفر).',
buy_fee:
'مبلغ اضافه (ریال) روی قیمت پایه برای سمت خرید. اگر خالی بگذارید، از «اختلاف پایه» استفاده می‌شود.',
'مبلغ اضافه (ریال) روی قیمت پایه برای سمت خرید. خالی یا صفر = بدون اختلاف.',
sell_fee:
'مبلغ کسر (ریال) از قیمت پایه برای سمت فروش. اگر خالی بگذارید، از «اختلاف پایه» استفاده می‌شود.',
'مبلغ کسر (ریال) از قیمت پایه برای سمت فروش. خالی یا صفر = بدون اختلاف.',
fee_service_charge_percentage:
'درصد کارمزد هر معامله (اعشاری؛ مثلاً ۰٫۰۰۵). روی همهٔ سفارش‌ها اعمال می‌شود و با اختلاف قیمت محصول یا buy_fee/sell_fee قاطی نمی‌شود. محصول مقداری: اگر مبلغ ریالی قفل باشد کارمزد از فلز، اگر وزن قفل باشد از ریال. محصول تعدادی: کارمزد فقط از ریال.',
fee_deduct_from_gold:
@@ -776,7 +777,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
zarin_gorohe_kar:
'کد gorooheKar هنگام ساخت/جستجوی کار در زرین. اگر چند پنل یک زرین دارند، برای هر پنل گروه کار متفاوت بگذارید.',
zarin_name_g:
'نام گروه حساب مشتریان در زرین (nameG). اگر چند پنل یک توکن دارند، برای هر پنل نام گروه متفاوت بگذارید. خالی = نام برند.',
'نام گروه حساب مشتریان در زرین (nameG). برای اپ خانگی معمولاً «مشتری نقدی (خریدار)» (گروه ۶). گروه همکار استفاده نشود چون مظنه فرق دارد. فقط از تنظیمات/دیپلوی تغییر می‌کند.',
zarin_uid_prefix:
'پیشوند عددی uid برای جلوگیری از تداخل بین چند پنل روی یک زرین. فقط عدد (مثلاً ۱۱).',
montify_name:
@@ -918,7 +919,6 @@ export const SETTING_LOCKED_KEYS = new Set([
'chart_id', 'chart_use_local_data',
'external_order_api_enabled',
'fee_deduct_from_gold',
'fee_single',
'referral_bonus_amount',
'group_sms',
'customer_direct_sms',