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_total: number
panel_registered: number panel_registered: number
panel_only: number panel_only: number
zarin_total: number zarin_total: number | null
zarin_only_count: number 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: { panel_only_list: {
data: PanelOnlyCustomer[] data: PanelOnlyCustomer[]
current_page: number current_page: number
@@ -75,7 +81,17 @@ export const getZarinCustomerSyncStatus = async (params?: {
page?: number page?: number
zarin_page?: number zarin_page?: number
include_zarin?: number | boolean 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 { export interface SyncPanelResult {
ok: boolean ok: boolean
@@ -18,11 +18,11 @@
<Icon icon="vuesax-linear:arrow-left" :size="16" /> <Icon icon="vuesax-linear:arrow-left" :size="16" />
<Icon icon="vuesax-linear:arrow-right" :size="16" /> <Icon icon="vuesax-linear:arrow-right" :size="16" />
</div> </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"> <div class="cs-stat__num">
<template v-if="status?.zarin_total != null">{{ status.zarin_total }}</template> <template v-if="status?.zarin_total != null">{{ status.zarin_total }}</template>
<a-spin v-else-if="loadingZarin" size="small" /> <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>
<div class="cs-stat__label">مشتریان زرین</div> <div class="cs-stat__label">مشتریان زرین</div>
</div> </div>
@@ -38,11 +38,11 @@
</div> </div>
<div class="cs-stat__label">پنل — بدون زرین</div> <div class="cs-stat__label">پنل — بدون زرین</div>
</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"> <div class="cs-stat__num">
<template v-if="status?.zarin_only_count != null">{{ status.zarin_only_count }}</template> <template v-if="status?.zarin_only_count != null">{{ status.zarin_only_count }}</template>
<a-spin v-else-if="loadingZarin" size="small" /> <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>
<div class="cs-stat__label">زرین — بدون پنل</div> <div class="cs-stat__label">زرین — بدون پنل</div>
</div> </div>
@@ -77,6 +77,7 @@
<Icon icon="vuesax-linear:arrow-right" :size="14" /> <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-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> </button>
</div> </div>
@@ -193,6 +194,10 @@
<span class="cs-card__head-title">مشتریان زرین بدون حساب پنل</span> <span class="cs-card__head-title">مشتریان زرین بدون حساب پنل</span>
</div> </div>
<div class="cs-card__head-actions"> <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 <a-button
type="primary" type="primary"
size="small" size="small"
@@ -214,6 +219,18 @@
</div> </div>
</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"> <div v-if="syncingZarin || zarinSyncResult" class="cs-result">
<template v-if="syncingZarin"> <template v-if="syncingZarin">
<a-spin size="small" /> <a-spin size="small" />
@@ -233,6 +250,12 @@
</div> </div>
<div v-if="loadingZarin" class="cs-loading"><a-spin /></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"> <template v-else-if="zarinOnlyList.length">
<!-- موبایل --> <!-- موبایل -->
<div class="cs-mobile-list"> <div class="cs-mobile-list">
@@ -300,7 +323,10 @@
</template> </template>
<div v-else class="cs-empty"> <div v-else class="cs-empty">
<div class="cs-empty__icon"><Icon icon="vuesax-linear:tick-circle" :size="28" /></div> <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>
</div> </div>
@@ -329,6 +355,7 @@ const importStore = useCustomerImportStore()
// ─── state ─── // ─── state ───
const loadingPanel = ref(true) // شروع با true تا اولین load کامل بشه const loadingPanel = ref(true) // شروع با true تا اولین load کامل بشه
const loadingZarin = ref(false) const loadingZarin = ref(false)
const zarinLoadFailed = ref(false)
const syncingPanel = ref(false) const syncingPanel = ref(false)
const syncingZarin = ref(false) const syncingZarin = ref(false)
const activeTab = ref<'panel' | 'zarin'>('panel') const activeTab = ref<'panel' | 'zarin'>('panel')
@@ -404,21 +431,51 @@ async function load(panelPage = 1) {
} }
// ─── load zarin customers (کند — فقط وقتی تب زرین باز میشه) ─── // ─── load zarin customers (کند — فقط وقتی تب زرین باز میشه) ───
async function loadZarin() { async function loadZarin(force = false) {
if (zarinFetched.value) return if (zarinFetched.value && !force) return
loadingZarin.value = true loadingZarin.value = true
zarinLoadFailed.value = false
try { try {
const res = await getZarinCustomerSyncStatus({ include_zarin: 1 } as any) const res = await getZarinCustomerSyncStatus({ include_zarin: 1 })
if (status.value) { 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_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 { } else {
status.value = res status.value = { ...res, zarin_fetch_error: null }
} }
zarinOnlyList.value = res.zarin_only_list?.data ?? [] zarinOnlyList.value = res.zarin_only_list?.data ?? []
zarinFetched.value = true zarinFetched.value = true
} catch (e) { console.error(e) } if (res.zarin_fetch_error || res.include_zarin === false) {
finally { loadingZarin.value = 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') { function onTabChange(tab: 'panel' | 'zarin') {
@@ -609,6 +666,11 @@ onMounted(() => {
&--zarin { background: rgba(14,165,233,0.25); } &--zarin { background: rgba(14,165,233,0.25); }
&--amber { background: rgba(251,191,36,0.2); } &--amber { background: rgba(251,191,36,0.2); }
&--sky { background: rgba(14,165,233,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 { &__num {
font-size: 20px; font-size: 20px;
@@ -702,6 +764,15 @@ onMounted(() => {
&--amber { background: rgba(245,158,11,0.15); color: #d97706; } &--amber { background: rgba(245,158,11,0.15); color: #d97706; }
&--sky { background: rgba(14,165,233,0.15); color: #0ea5e9; } &--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-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 { .cs-mobile-list {
display: block; 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; } &__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); } &__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 { .cs-pagination {
@@ -179,6 +179,7 @@ export function supportMessengerIdPlaceholder(messenger?: string): string {
export const HIDDEN_LEGACY_SETTING_KEYS = new Set([ export const HIDDEN_LEGACY_SETTING_KEYS = new Set([
'main_product_price', 'main_product_price',
'montzar_price', 'montzar_price',
'fee_single',
'etehadie_api_url', 'etehadie_api_url',
'goldir_price', 'goldir_price',
'price_primary_source', 'price_primary_source',
@@ -566,11 +567,11 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
show_product_details: show_product_details:
'روشن: بعد از انتخاب محصول در صفحه خرید/فروش، متن توضیح محصول نمایش داده می‌شود. خاموش: توضیح نشان داده نمی‌شود.', 'روشن: بعد از انتخاب محصول در صفحه خرید/فروش، متن توضیح محصول نمایش داده می‌شود. خاموش: توضیح نشان داده نمی‌شود.',
fee_single: fee_single:
'اختلاف پایه (ریال) بین قیمت خرید و فروش روی مظنه. معمولاً نصف مجموع اختلاف قیمت خرید و فروش است.', 'منسوخ — دیگر استفاده نمی‌شود. اختلاف فقط از buy_fee / sell_fee (خالی = صفر).',
buy_fee: buy_fee:
'مبلغ اضافه (ریال) روی قیمت پایه برای سمت خرید. اگر خالی بگذارید، از «اختلاف پایه» استفاده می‌شود.', 'مبلغ اضافه (ریال) روی قیمت پایه برای سمت خرید. خالی یا صفر = بدون اختلاف.',
sell_fee: sell_fee:
'مبلغ کسر (ریال) از قیمت پایه برای سمت فروش. اگر خالی بگذارید، از «اختلاف پایه» استفاده می‌شود.', 'مبلغ کسر (ریال) از قیمت پایه برای سمت فروش. خالی یا صفر = بدون اختلاف.',
fee_service_charge_percentage: fee_service_charge_percentage:
'درصد کارمزد هر معامله (اعشاری؛ مثلاً ۰٫۰۰۵). روی همهٔ سفارش‌ها اعمال می‌شود و با اختلاف قیمت محصول یا buy_fee/sell_fee قاطی نمی‌شود. محصول مقداری: اگر مبلغ ریالی قفل باشد کارمزد از فلز، اگر وزن قفل باشد از ریال. محصول تعدادی: کارمزد فقط از ریال.', 'درصد کارمزد هر معامله (اعشاری؛ مثلاً ۰٫۰۰۵). روی همهٔ سفارش‌ها اعمال می‌شود و با اختلاف قیمت محصول یا buy_fee/sell_fee قاطی نمی‌شود. محصول مقداری: اگر مبلغ ریالی قفل باشد کارمزد از فلز، اگر وزن قفل باشد از ریال. محصول تعدادی: کارمزد فقط از ریال.',
fee_deduct_from_gold: fee_deduct_from_gold:
@@ -776,7 +777,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
zarin_gorohe_kar: zarin_gorohe_kar:
'کد gorooheKar هنگام ساخت/جستجوی کار در زرین. اگر چند پنل یک زرین دارند، برای هر پنل گروه کار متفاوت بگذارید.', 'کد gorooheKar هنگام ساخت/جستجوی کار در زرین. اگر چند پنل یک زرین دارند، برای هر پنل گروه کار متفاوت بگذارید.',
zarin_name_g: zarin_name_g:
'نام گروه حساب مشتریان در زرین (nameG). اگر چند پنل یک توکن دارند، برای هر پنل نام گروه متفاوت بگذارید. خالی = نام برند.', 'نام گروه حساب مشتریان در زرین (nameG). برای اپ خانگی معمولاً «مشتری نقدی (خریدار)» (گروه ۶). گروه همکار استفاده نشود چون مظنه فرق دارد. فقط از تنظیمات/دیپلوی تغییر می‌کند.',
zarin_uid_prefix: zarin_uid_prefix:
'پیشوند عددی uid برای جلوگیری از تداخل بین چند پنل روی یک زرین. فقط عدد (مثلاً ۱۱).', 'پیشوند عددی uid برای جلوگیری از تداخل بین چند پنل روی یک زرین. فقط عدد (مثلاً ۱۱).',
montify_name: montify_name:
@@ -918,7 +919,6 @@ export const SETTING_LOCKED_KEYS = new Set([
'chart_id', 'chart_use_local_data', 'chart_id', 'chart_use_local_data',
'external_order_api_enabled', 'external_order_api_enabled',
'fee_deduct_from_gold', 'fee_deduct_from_gold',
'fee_single',
'referral_bonus_amount', 'referral_bonus_amount',
'group_sms', 'group_sms',
'customer_direct_sms', 'customer_direct_sms',