fix bugs and added some feature for withdraw handling

This commit is contained in:
2026-07-19 15:49:52 +03:30
parent b446c21afc
commit 3cb1e1a0b0
17 changed files with 442 additions and 58 deletions
+86 -25
View File
@@ -13,8 +13,23 @@ import {
function pickConstRow(res: Const | Const[] | null | undefined): Const | null { function pickConstRow(res: Const | Const[] | null | undefined): Const | null {
if (!res) return null if (!res) return null
if (Array.isArray(res)) return (res[0] as Const) ?? null if (Array.isArray(res)) {
return res const first = (res[0] as Const) ?? null
return isMeaningfulConstRow(first) ? first : null
}
return isMeaningfulConstRow(res) ? res : null
}
/** API برای کلید ناموجود گاهی `{}` یا null برمی‌گرداند — نباید مهاجرت را قطع کند */
function isMeaningfulConstRow(row: Const | null | undefined): row is Const {
if (!row || typeof row !== 'object') return false
const hasId = row.id != null || row.const_id != null
const raw = row.value
const hasValue =
typeof raw === 'string'
? raw.trim() !== ''
: raw != null && !(typeof raw === 'object' && !Array.isArray(raw) && Object.keys(raw as object).length === 0)
return hasId || hasValue
} }
function parseLegacySlider(value: string | null | undefined): IBenefits[] { function parseLegacySlider(value: string | null | undefined): IBenefits[] {
@@ -33,24 +48,45 @@ export type AppBannersConstState = {
data: AppBannersValue data: AppBannersValue
} }
async function loadLegacySliderInto(merged: AppBannersValue): Promise<void> {
try {
const sliderRes = await useAxios.get<Const | Const[]>(
`/api/const/key/${LEGACY_SLIDER_BANNERS_CONST_KEY}`
)
const sliderRow = pickConstRow(sliderRes)
if (!sliderRow) return
const slides = parseLegacySlider(sliderRow.value)
if (slides.length) merged.slider = slides
} catch {
// ignore
}
}
/** /**
* یک API واحد برای همهٔ بنرها. * یک API واحد برای همهٔ بنرها.
* اگر `app_banners` نبود، از کلیدهای قدیمی migrate می‌کند (فقط خواندن). * اگر `app_banners` نبود یا اسلایدر خالی بود، از کلیدهای قدیمی migrate می‌کند (فقط خواندن).
*/ */
export async function loadAppBannersConst(): Promise<AppBannersConstState> { export async function loadAppBannersConst(): Promise<AppBannersConstState> {
let appRow: Const | null = null
try { try {
const res = await useAxios.get<Const | Const[]>(`/api/const/key/${APP_BANNERS_CONST_KEY}`) const res = await useAxios.get<Const | Const[]>(`/api/const/key/${APP_BANNERS_CONST_KEY}`)
const row = pickConstRow(res) appRow = pickConstRow(res)
if (row) {
return {
id: row.id ?? row.const_id ?? null,
data: parseAppBanners(row.value),
}
}
} catch { } catch {
// ادامه با مهاجرت از کلیدهای قدیمی // ادامه با مهاجرت از کلیدهای قدیمی
} }
if (appRow) {
const data = parseAppBanners(appRow.value)
// رکورد جدید خالی: اسلایدر را از header_banner پر کن
if (!data.slider.length) {
await loadLegacySliderInto(data)
}
return {
id: appRow.id ?? appRow.const_id ?? null,
data,
}
}
const merged = emptyAppBanners() const merged = emptyAppBanners()
try { try {
@@ -76,23 +112,39 @@ export async function loadAppBannersConst(): Promise<AppBannersConstState> {
// ignore // ignore
} }
try { await loadLegacySliderInto(merged)
const sliderRes = await useAxios.get<Const | Const[]>(
`/api/const/key/${LEGACY_SLIDER_BANNERS_CONST_KEY}`
)
const sliderRow = pickConstRow(sliderRes)
if (sliderRow) {
const slides = parseLegacySlider(sliderRow.value)
if (slides.length) merged.slider = slides
}
} catch {
// ignore
}
// id=null → اولین ذخیره، رکورد جدید با key=app_banners می‌سازد // id=null → اولین ذخیره، رکورد جدید با key=app_banners می‌سازد
return { id: null, data: merged } return { id: null, data: merged }
} }
async function syncLegacySliderConst(slides: IBenefits[]): Promise<void> {
const value = JSON.stringify(slides ?? [])
const payload = {
key: LEGACY_SLIDER_BANNERS_CONST_KEY,
value,
title_fa: 'بنرهای اسلایدر',
is_active: true,
description: 'اسلایدر بالای صفحه اصلی اپ مشتری (سازگاری با اپ)',
}
try {
const existingRes = await useAxios.get<Const | Const[]>(
`/api/const/key/${LEGACY_SLIDER_BANNERS_CONST_KEY}`
)
const existing = pickConstRow(existingRes)
const existingId = existing?.id ?? existing?.const_id ?? null
if (existingId) {
await useAxios.put<Const>(`/admin/const/${existingId}`, { id: existingId, ...payload })
return
}
} catch {
// create below
}
await useAxios.post<Const>(`/admin/const`, { id: 0, ...payload })
}
export async function saveAppBannersConst( export async function saveAppBannersConst(
id: number | null, id: number | null,
data: AppBannersValue data: AppBannersValue
@@ -106,11 +158,20 @@ export async function saveAppBannersConst(
description: 'همهٔ بنرهای اپ: اسلایدر صفحه اصلی + تصاویر تم لایت/دارک', description: 'همهٔ بنرهای اپ: اسلایدر صفحه اصلی + تصاویر تم لایت/دارک',
} }
let nextId = id
if (id) { if (id) {
await useAxios.put<Const>(`/admin/const/${id}`, { id, ...payload }) await useAxios.put<Const>(`/admin/const/${id}`, { id, ...payload })
return id } else {
const created = await useAxios.post<Const>(`/admin/const`, { id: 0, ...payload })
nextId = created?.id ?? created?.const_id ?? null
} }
const created = await useAxios.post<Const>(`/admin/const`, { id: 0, ...payload }) // اپ مشتری هنوز header_banner می‌خواند — اسلایدر را همگام نگه دار
return created?.id ?? created?.const_id ?? null try {
await syncLegacySliderConst(data.slider ?? [])
} catch (error) {
console.error('همگام‌سازی header_banner ناموفق بود:', error)
}
return nextId
} }
+17 -4
View File
@@ -2,7 +2,6 @@
<div class="banners-page"> <div class="banners-page">
<div class="banners-page__body"> <div class="banners-page__body">
<a-collapse <a-collapse
v-if="!showSettingsProfileBanners"
v-model:activeKey="sliderActiveKey" v-model:activeKey="sliderActiveKey"
:bordered="false" :bordered="false"
class="banners-page__collapse" class="banners-page__collapse"
@@ -145,12 +144,26 @@ import {
bannersShareHelp, bannersShareHelp,
bannersShowHelp, bannersShowHelp,
} from '@/utils/adminHelpCopy' } from '@/utils/adminHelpCopy'
import { computed, onMounted, ref, watch } from 'vue'
const { state, saving, save } = useAppBannersAdmin() const { state, saving, save, load } = useAppBannersAdmin()
/** پیش‌فرض کالپس بنرهای اسلایدر بسته است */ /** اگر اسلایدری هست، پنل را باز نگه دار تا خالی به‌نظر نرسد */
const sliderActiveKey = ref<(string | number)[]>([]) const sliderActiveKey = ref<(string | number)[]>(['slider'])
const showSettingsProfileBanners = computed(() => hasAppUiConfig()) const showSettingsProfileBanners = computed(() => hasAppUiConfig())
watch(
() => state.slider.length,
(len) => {
if (len > 0 && !sliderActiveKey.value.includes('slider')) {
sliderActiveKey.value = ['slider']
}
}
)
onMounted(() => {
void load(true)
})
const typeList = ref([ const typeList = ref([
{ lable: 'بدون رویداد', value: null }, { lable: 'بدون رویداد', value: null },
{ lable: 'باز شدن لینک', value: 'link' }, { lable: 'باز شدن لینک', value: 'link' },
@@ -117,6 +117,7 @@ import {
serializeReygiriSites, serializeReygiriSites,
serializeTrackingSystemsEnabled, serializeTrackingSystemsEnabled,
} from '@/utils/reygiriSites' } from '@/utils/reygiriSites'
import { setTrackingSystemsEnabledFromSettings } from '@/utils/config'
const sites = ref<CaptureSite[]>([]) const sites = ref<CaptureSite[]>([])
const loading = ref(false) const loading = ref(false)
@@ -224,6 +225,7 @@ async function saveEnabled(checked: boolean) {
const value = serializeTrackingSystemsEnabled(checked) const value = serializeTrackingSystemsEnabled(checked)
await setConfiguration(buildSettingSavePayload(row, value)) await setConfiguration(buildSettingSavePayload(row, value))
enabledRow.value = { ...row, value } enabledRow.value = { ...row, value }
setTrackingSystemsEnabledFromSettings(checked)
message.success('وضعیت نمایش سایت‌ها ذخیره شد') message.success('وضعیت نمایش سایت‌ها ذخیره شد')
} catch (error) { } catch (error) {
console.error(error) console.error(error)
+8 -2
View File
@@ -174,7 +174,7 @@ import SettingValueField from './SettingValueField.vue'
import SettingCard from './SettingCard.vue' import SettingCard from './SettingCard.vue'
import { refreshAppModeMenu } from '@/router' import { refreshAppModeMenu } from '@/router'
import { useUserStore } from '@/store/userStore' import { useUserStore } from '@/store/userStore'
import { applyBannerEnabledFromRows, setBannerEnabledFromSettings, setStoreModeFromSettings } from '@/utils/config' import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, setBannerEnabledFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings } from '@/utils/config'
import { import {
EDITABLE_SETTING_GROUPS, EDITABLE_SETTING_GROUPS,
SYSTEM_SETTING_GROUPS, SYSTEM_SETTING_GROUPS,
@@ -424,7 +424,10 @@ async function initConfig() {
baseData.value = rows.map(normalizeSettingRow) baseData.value = rows.map(normalizeSettingRow)
if (isSystemMode.value) { if (isSystemMode.value) {
applyBannerEnabledFromRows(baseData.value) applyBannerEnabledFromRows(baseData.value)
applyTrackingSystemsEnabledFromRows(baseData.value)
} else { } else {
applyBannerEnabledFromRows(baseData.value)
applyTrackingSystemsEnabledFromRows(baseData.value)
const storeRow = baseData.value.find((row) => row.key === 'is_store') const storeRow = baseData.value.find((row) => row.key === 'is_store')
if (storeRow) { if (storeRow) {
setStoreModeFromSettings(isSwitchOn(storeRow.value)) setStoreModeFromSettings(isSwitchOn(storeRow.value))
@@ -535,9 +538,12 @@ async function saveSettingRow(row: Const) {
setStoreModeFromSettings(isSwitchOn(value)) setStoreModeFromSettings(isSwitchOn(value))
refreshMenuAfterStoreChange() refreshMenuAfterStoreChange()
} }
if (row.key === 'banner_enabled' && isSystemMode.value) { if (row.key === 'banner_enabled') {
setBannerEnabledFromSettings(isSwitchOn(value)) setBannerEnabledFromSettings(isSwitchOn(value))
} }
if (row.key === 'tracking_systems_enabled') {
setTrackingSystemsEnabledFromSettings(isSwitchOn(value))
}
savedFlashIds.value.add(id) savedFlashIds.value.add(id)
if (saveTimers.has(id)) clearTimeout(saveTimers.get(id)) if (saveTimers.has(id)) clearTimeout(saveTimers.get(id))
saveTimers.set( saveTimers.set(
@@ -12,7 +12,7 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
title: 'عمومی و برند', title: 'عمومی و برند',
icon: 'vuesax-linear:home-2', icon: 'vuesax-linear:home-2',
match: (k) => match: (k) =>
/^(name_fa|name_en|bace_url|server_url|logo_|title_pwa|title_fav|open_register|auto_active_customer|store_status|key$)/.test( /^(name_fa|name_en|bace_url|server_url|logo_|title_pwa|title_fav|open_register|auto_active_customer|store_status|key$|tracking_systems_enabled|banner_enabled)$/.test(
k k
) )
}, },
@@ -119,7 +119,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
title: 'عمومی و برند', title: 'عمومی و برند',
icon: 'vuesax-linear:home-2', icon: 'vuesax-linear:home-2',
match: (k) => match: (k) =>
/^(name_fa|name_en|app_url|admin_url|apk_download_link|banner_enabled|permision__enabled|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|main_metal|unit$|open_register|auto_active_customer|store_status|key$|is_store|group_sms|customer_direct_sms|has_page_earn_money|sms_panel)/.test( /^(name_fa|name_en|app_url|admin_url|apk_download_link|permision__enabled|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|main_metal|unit$|open_register|auto_active_customer|store_status|key$|is_store|group_sms|customer_direct_sms|has_page_earn_money|sms_panel)/.test(
k k
) )
}, },
@@ -176,7 +176,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
id: 'infra', id: 'infra',
title: 'زیرساخت', title: 'زیرساخت',
icon: 'vuesax-linear:cpu', icon: 'vuesax-linear:cpu',
match: (k) => /^(wss_enabled|sse_path|profit_gold_api_enabled|tracking_systems|tracking_systems_enabled)$/.test(k) match: (k) => /^(wss_enabled|sse_path|profit_gold_api_enabled)$/.test(k)
}, },
{ {
id: 'misc', id: 'misc',
@@ -187,6 +187,8 @@ export const HIDDEN_LEGACY_SETTING_KEYS = new Set([
'service_charge_percentage', 'service_charge_percentage',
'support_messenger', 'support_messenger',
'support_messenger_id', 'support_messenger_id',
// JSON از صفحهٔ اختصاصی «سایت‌های ری‌گیری» مدیریت می‌شود؛ سوییچ نمایش در تنظیمات می‌ماند
'tracking_systems',
]) ])
/** فقط وقتی is_store فعال است در پنل دیده می‌شوند */ /** فقط وقتی is_store فعال است در پنل دیده می‌شوند */
@@ -234,6 +236,7 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
sell_fee: 'price', sell_fee: 'price',
fee_free: 'number', fee_free: 'number',
min_withdraw: 'price', min_withdraw: 'price',
min_deposit: 'price',
min_first_buy: 'price', min_first_buy: 'price',
minimum_sell: 'price', minimum_sell: 'price',
gift_for_referrer: 'number', gift_for_referrer: 'number',
@@ -514,7 +517,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
name_fa: name_fa:
'نام فارسی برند که در پیامک‌ها، عنوان صفحات و بخش‌های عمومی اپ به مشتری نشان داده می‌شود.', 'نام فارسی برند که در پیامک‌ها، عنوان صفحات و بخش‌های عمومی اپ به مشتری نشان داده می‌شود.',
banner_enabled: banner_enabled:
'روشن: منوی «بنرها» در تنظیمات فعال می‌شود و می‌توانید اسلایدر بالای صفحهٔ اصلی اپ را مدیریت کنید. خاموش: بنرها در اپ نشان داده نمی‌شوند.', 'روشن: منوی «بنرها» در ادمین و اسلایدر صفحه اصلی اپ مشتری نمایش داده می‌شود. خاموش: مخفی است.',
permision__enabled: permision__enabled:
'روشن: آیتم «مجوز ها» در منوی تنظیمات اپ خانگی (تم جدید) نمایش داده می‌شود. خاموش: مخفی است.', 'روشن: آیتم «مجوز ها» در منوی تنظیمات اپ خانگی (تم جدید) نمایش داده می‌شود. خاموش: مخفی است.',
open_register: open_register:
@@ -549,6 +552,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
'تعداد اولین سفارش‌های هر کاربر که از کارمزد معاف می‌شوند. صفر یعنی غیرفعال.', 'تعداد اولین سفارش‌های هر کاربر که از کارمزد معاف می‌شوند. صفر یعنی غیرفعال.',
min_withdraw: min_withdraw:
'حداقل مبلغ درخواست برداشت (ریال). درخواست‌های کمتر پذیرفته نمی‌شوند.', 'حداقل مبلغ درخواست برداشت (ریال). درخواست‌های کمتر پذیرفته نمی‌شوند.',
min_deposit:
'حداقل مبلغ واریز به کیف پول (ریال). مبالغ کمتر در اپ و درگاه پذیرفته نمی‌شوند.',
min_first_buy: min_first_buy:
'حداقل مبلغ اولین خرید هر کاربر (ریال).', 'حداقل مبلغ اولین خرید هر کاربر (ریال).',
minimum_sell: minimum_sell:
@@ -608,9 +613,9 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
support_messengers: support_messengers:
'فقط مواردی که در این لیست ثبت شوند در منوی شناور پشتیبانی اپ مشتری نمایش داده می‌شوند. برای تماس تلفنی یا پیامک، نوع «تماس تلفنی» / «پیامک» را با شماره اضافه کنید.', 'فقط مواردی که در این لیست ثبت شوند در منوی شناور پشتیبانی اپ مشتری نمایش داده می‌شوند. برای تماس تلفنی یا پیامک، نوع «تماس تلفنی» / «پیامک» را با شماره اضافه کنید.',
tracking_systems: tracking_systems:
'لیست JSON سامانه‌های ردیابی/آنالیتیکس در اپ مشتری. هر مورد: type (gtag، gtm، clarity، custom)، id (کد اندازه‌گیری یا شناسه پروژه)، enabled (روشن/خاموش). فقط از صفحه دیپلوی قابل ویرایش است.', 'لیست سایت‌های ری‌گیری (name/url) — از منوی «سایت‌های ری‌گیری» ویرایش کنید.',
tracking_systems_enabled: tracking_systems_enabled:
'روشن: وقتی tracking_systems پر باشد، اسکریپت‌های ردیابی در اپ مشتری بارگذاری می‌شوند. خاموش: هیچ سامانه ردیابی نمایش داده نمی‌شود — حتی اگر JSON پر باشد.', 'روشن: منوی «سایت‌های ری‌گیری» در ادمین و اپ مشتری نمایش داده می‌شود. خاموش: مخفی است.',
support_messenger: support_messenger:
'پیام‌رسانی که آیکون کناری و دکمه شناور پشتیبانی در اپ مشتری (درباره ما، قوانین، آموزش و راهنمای واریز) به آن هدایت می‌کند.', 'پیام‌رسانی که آیکون کناری و دکمه شناور پشتیبانی در اپ مشتری (درباره ما، قوانین، آموزش و راهنمای واریز) به آن هدایت می‌کند.',
support_messenger_id: support_messenger_id:
@@ -771,6 +776,7 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
fee_deduct_from_gold: 'اعمال مبلغ دقیق در معامله مشتری', fee_deduct_from_gold: 'اعمال مبلغ دقیق در معامله مشتری',
fee_free: 'معافیت کارمزد سفارش اول', fee_free: 'معافیت کارمزد سفارش اول',
min_withdraw: 'حداقل مبلغ برداشت', min_withdraw: 'حداقل مبلغ برداشت',
min_deposit: 'حداقل مبلغ واریز کیف پول',
min_first_buy: 'حداقل اولین خرید', min_first_buy: 'حداقل اولین خرید',
minimum_sell: 'حداقل مبلغ فروش (ریال)', minimum_sell: 'حداقل مبلغ فروش (ریال)',
referral_id: 'کد معرف اجباری', referral_id: 'کد معرف اجباری',
@@ -808,8 +814,8 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
send_to_tahesab: 'ارسال خودکار به ته‌حساب (قدیمی)', send_to_tahesab: 'ارسال خودکار به ته‌حساب (قدیمی)',
send_to_pargar: 'ارسال خودکار به پرگار (قدیمی)', send_to_pargar: 'ارسال خودکار به پرگار (قدیمی)',
support_messengers: 'پیام‌رسان‌های پشتیبانی', support_messengers: 'پیام‌رسان‌های پشتیبانی',
tracking_systems: 'سامانه‌های ردیابی', tracking_systems: 'سایت‌های ری‌گیری',
tracking_systems_enabled: 'نمایش سامانه‌های ردیابی', tracking_systems_enabled: 'نمایش سایت‌های ری‌گیری',
permision__enabled: 'نمایش منوی مجوزها', permision__enabled: 'نمایش منوی مجوزها',
support_messenger: 'پیام‌رسان پشتیبانی', support_messenger: 'پیام‌رسان پشتیبانی',
support_messenger_id: 'شناسه پشتیبانی', support_messenger_id: 'شناسه پشتیبانی',
@@ -891,7 +897,7 @@ export const SETTING_EDITABLE_KEYS = new Set([
'invite_link_text', 'invite_link_text',
'jibit_verification', 'jibit_verification',
'main_product_price', 'main_product_price',
'min_first_buy', 'min_withdraw', 'minimum_sell', 'min_first_buy', 'min_withdraw', 'min_deposit', 'minimum_sell',
'montzar_mobile', 'montzar_open_buy', 'montzar_open_sell', 'montzar_mobile', 'montzar_open_buy', 'montzar_open_sell',
'montzar_password', 'montzar_price', 'montzar_product_id', 'montzar_password', 'montzar_price', 'montzar_product_id',
'open_register', 'open_register',
@@ -0,0 +1,107 @@
<template>
<BasicModal
v-model:visible="visibleProxy"
:width="isMobile ? '92vw' : '28rem'"
title="ویرایش کد حسابداری"
ok-text="ذخیره"
:loading="loading"
@ok="submit"
@close="close"
>
<div class="flex flex-col gap-3">
<p class="m-0 text-sm" style="color: var(--admin-text-secondary)">
مشتری:
<strong class="text-[var(--admin-text)]">{{ customer?.name || '—' }}</strong>
</p>
<a-form layout="vertical">
<a-form-item label="کد حسابداری" class="mb-0">
<BasicInput
v-model:value="code"
placeholder="کد حسابداری را وارد کنید"
dir="ltr"
class="text-left"
/>
</a-form-item>
</a-form>
<p class="m-0 text-xs" style="color: var(--admin-text-secondary)">
برای پاک کردن کد، فیلد را خالی بگذارید و ذخیره کنید.
</p>
</div>
</BasicModal>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { message } from 'ant-design-vue'
import BasicModal from '@/components/BasicModal.vue'
import { updateCustomer } from '@/api/customer'
import type { Customer } from '@/model/customer'
import type { AccountingProvider } from '@/utils/config'
import { displayAccountingCode, normalizeAccountingCode } from '@/utils/accountingCode'
import { isMobile } from '@core/utils/is'
const props = defineProps<{
visible: boolean
customer: Customer | null
provider: AccountingProvider
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'success'): void
}>()
const loading = ref(false)
const code = ref('')
const visibleProxy = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value),
})
function currentCodeOf(customer: Customer | null): string {
if (!customer) return ''
if (props.provider === 'pargar') return displayAccountingCode(customer.pargar_account_id)
if (props.provider === 'tahesab') return displayAccountingCode(customer.moshtari_code)
return ''
}
watch(
() => [props.visible, props.customer, props.provider] as const,
([visible]) => {
if (!visible) return
const shown = currentCodeOf(props.customer)
code.value = shown === '-' ? '' : shown
}
)
function close() {
visibleProxy.value = false
}
async function submit() {
if (!props.customer?.id) return
if (props.provider !== 'pargar' && props.provider !== 'tahesab') {
message.error('حسابداری فعال نیست.')
return
}
const normalized = normalizeAccountingCode(code.value)
const payload =
props.provider === 'pargar'
? { pargar_account_id: normalized }
: { moshtari_code: normalized }
try {
loading.value = true
await updateCustomer(props.customer.id, payload as Customer)
message.success('کد حسابداری ذخیره شد.')
emit('success')
close()
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
</script>
@@ -29,6 +29,7 @@
@toggleTrade="forward('toggleTrade', $event)" @toggleTrade="forward('toggleTrade', $event)"
@confirmUser="forwardAndClose('confirmUser', $event)" @confirmUser="forwardAndClose('confirmUser', $event)"
@sendSms="forwardAndClose('sendSms', $event)" @sendSms="forwardAndClose('sendSms', $event)"
@editAccounting="forwardAndClose('editAccounting', $event)"
/> />
</template> </template>
@@ -59,6 +60,7 @@ const emit = defineEmits<{
(e: 'toggleTrade', item: Customer): void (e: 'toggleTrade', item: Customer): void
(e: 'confirmUser', item: Customer): void (e: 'confirmUser', item: Customer): void
(e: 'sendSms', item: Customer): void (e: 'sendSms', item: Customer): void
(e: 'editAccounting', item: Customer): void
}>() }>()
const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<Customer>() const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<Customer>()
@@ -72,6 +74,7 @@ type SheetAction =
| 'toggleTrade' | 'toggleTrade'
| 'confirmUser' | 'confirmUser'
| 'sendSms' | 'sendSms'
| 'editAccounting'
function forward(action: SheetAction, item: Customer) { function forward(action: SheetAction, item: Customer) {
emit(action, item) emit(action, item)
@@ -37,6 +37,15 @@
</BaseButton> </BaseButton>
<div class="customer-mobile-sheet__actions"> <div class="customer-mobile-sheet__actions">
<BaseButton
v-if="accountingCodeLabel"
size="small"
type="default"
icon="vuesax-linear:card-edit"
@click="closeAndEmit('editAccounting', row)"
>
کد حسابداری
</BaseButton>
<BaseButton size="small" type="default" icon="vuesax-linear:stickynote" @click="closeAndEmit('info', row)"> <BaseButton size="small" type="default" icon="vuesax-linear:stickynote" @click="closeAndEmit('info', row)">
مشخصات مشخصات
</BaseButton> </BaseButton>
@@ -84,6 +93,11 @@
<MobileInfoGrid> <MobileInfoGrid>
<MobileDetailRow label="کد کاربری" :value="row.id ?? '-'" /> <MobileDetailRow label="کد کاربری" :value="row.id ?? '-'" />
<MobileDetailRow
v-if="accountingCodeLabel"
:label="accountingCodeLabel"
:value="accountingCodeOf(row)"
/>
<MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" /> <MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" />
<MobileDetailRow label="نام معرف" :value="row.referral?.name ?? '-'" /> <MobileDetailRow label="نام معرف" :value="row.referral?.name ?? '-'" />
<MobileDetailRow label="آخرین بازدید" :value="lastSeen(row)" value-class="text-xs" /> <MobileDetailRow label="آخرین بازدید" :value="lastSeen(row)" value-class="text-xs" />
@@ -94,7 +108,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { Modal } from 'ant-design-vue' import { Modal } from 'ant-design-vue'
import type { Customer } from '@/model/customer' import type { Customer } from '@/model/customer'
import type { ChargeTarget } from '@/model/wallet' import type { ChargeTarget } from '@/model/wallet'
@@ -105,7 +119,13 @@ import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue'
import MobileInfoGrid from '@/components/mobile/MobileInfoGrid.vue' import MobileInfoGrid from '@/components/mobile/MobileInfoGrid.vue'
import CustomerWalletBalancePanel from '@/components/customer/CustomerWalletBalancePanel.vue' import CustomerWalletBalancePanel from '@/components/customer/CustomerWalletBalancePanel.vue'
import { toJalali } from '@core/utils/useDataTime' import { toJalali } from '@core/utils/useDataTime'
import { customerDirectSmsEnabledSetting } from '@/utils/config' import {
accountingProviderSetting,
customerDirectSmsEnabledSetting,
initAccountingSettingsFromApi
} from '@/utils/config'
import { displayAccountingCode } from '@/utils/accountingCode'
import { displayAccountingCode } from '@/utils/accountingCode'
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
@@ -122,11 +142,29 @@ const emit = defineEmits<{
(e: 'toggleTrade', item: Customer): void (e: 'toggleTrade', item: Customer): void
(e: 'confirmUser', item: Customer): void (e: 'confirmUser', item: Customer): void
(e: 'sendSms', item: Customer): void (e: 'sendSms', item: Customer): void
(e: 'editAccounting', item: Customer): void
}>() }>()
const chargeTargets = ref<ChargeTarget[]>([]) const chargeTargets = ref<ChargeTarget[]>([])
const customerDirectSmsEnabled = customerDirectSmsEnabledSetting const customerDirectSmsEnabled = customerDirectSmsEnabledSetting
const accountingCodeLabel = computed(() => {
const provider = accountingProviderSetting.value
if (provider === 'pargar' || provider === 'tahesab') return 'کد حسابداری'
return null
})
function accountingCodeOf(row: Customer) {
const provider = accountingProviderSetting.value
if (provider === 'pargar') return displayAccountingCode(row.pargar_account_id)
if (provider === 'tahesab') return displayAccountingCode(row.moshtari_code)
return '-'
}
onMounted(() => {
void initAccountingSettingsFromApi()
})
const visibleProxy = computed({ const visibleProxy = computed({
get: () => props.visible, get: () => props.visible,
set: (value: boolean) => emit('update:visible', value), set: (value: boolean) => emit('update:visible', value),
@@ -134,7 +172,7 @@ const visibleProxy = computed({
const sheetTitle = computed(() => props.item?.name?.trim() || 'مشتری') const sheetTitle = computed(() => props.item?.name?.trim() || 'مشتری')
type SheetEmit = 'archive' | 'chargeWallet' | 'info' | 'confirmUser' | 'edit' | 'sendSms' type SheetEmit = 'archive' | 'chargeWallet' | 'info' | 'confirmUser' | 'edit' | 'sendSms' | 'editAccounting'
function closeAndEmit(event: SheetEmit, row: Customer) { function closeAndEmit(event: SheetEmit, row: Customer) {
visibleProxy.value = false visibleProxy.value = false
@@ -19,6 +19,17 @@
افزودن مشتری افزودن مشتری
</BaseButton> </BaseButton>
</template> </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 }"> <template #action="{ record }">
<div class="gap-2 hidden md:flex justify-center"> <div class="gap-2 hidden md:flex justify-center">
<a-tooltip :key="record.is_archive" placement="bottom" class="cursor-pointer"> <a-tooltip :key="record.is_archive" placement="bottom" class="cursor-pointer">
@@ -54,7 +65,7 @@
<Icon icon="vuesax-linear:wallet-add" color="#9333ea" /> <Icon icon="vuesax-linear:wallet-add" color="#9333ea" />
</a-tooltip> </a-tooltip>
<a-tooltip <a-tooltip
v-if="tahesabEnabled && !record.moshtari_code" v-if="tahesabEnabled && !isValidAccountingCode(record.moshtari_code)"
placement="bottom" placement="bottom"
class="cursor-pointer" class="cursor-pointer"
@click="openTahesabRegister(record)" @click="openTahesabRegister(record)"
@@ -63,7 +74,7 @@
<Icon icon="vuesax-linear:profile-add" color="#CA8A04" /> <Icon icon="vuesax-linear:profile-add" color="#CA8A04" />
</a-tooltip> </a-tooltip>
<a-tooltip <a-tooltip
v-if="pargarEnabled && !record.pargar_account_id" v-if="pargarEnabled && !isValidAccountingCode(record.pargar_account_id)"
placement="bottom" placement="bottom"
class="cursor-pointer" class="cursor-pointer"
@click="openPargarRegister(record)" @click="openPargarRegister(record)"
@@ -71,6 +82,15 @@
<template #title><span>در پرگار ثبت نشده — کلیک برای ثبت</span></template> <template #title><span>در پرگار ثبت نشده — کلیک برای ثبت</span></template>
<Icon icon="vuesax-linear:profile-add" color="#7C3AED" /> <Icon icon="vuesax-linear:profile-add" color="#7C3AED" />
</a-tooltip> </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)"> <a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)">
<template #title><span>مشخصات</span></template> <template #title><span>مشخصات</span></template>
<Icon icon="vuesax-linear:stickynote" color="#10913A" /> <Icon icon="vuesax-linear:stickynote" color="#10913A" />
@@ -127,11 +147,19 @@
@toggleTrade="(item) => changeCustomerOrderActivationFunction(item.id)" @toggleTrade="(item) => changeCustomerOrderActivationFunction(item.id)"
@confirmUser="openConfirmUser" @confirmUser="openConfirmUser"
@sendSms="openDirectSms" @sendSms="openDirectSms"
@editAccounting="openAccountingCodeEdit"
/> />
</div> </div>
<CustomerDirectSmsModal v-model:visible="directSmsVisible" :customer="directSmsCustomer" /> <CustomerDirectSmsModal v-model:visible="directSmsVisible" :customer="directSmsCustomer" />
<CustomerAccountingCodeModal
v-model:visible="accountingCodeVisible"
:customer="accountingCodeCustomer"
:provider="accountingProviderSetting"
@success="fetch"
/>
<BasicModal <BasicModal
v-model:visible="confirmVisible" v-model:visible="confirmVisible"
:width="isMobile ? '92vw' : '50vw'" :width="isMobile ? '92vw' : '50vw'"
@@ -178,7 +206,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { message, Modal } from 'ant-design-vue' import { message, Modal } from 'ant-design-vue'
import { ref } from 'vue' import { onMounted, ref, computed } from 'vue'
import { import {
changeCustomerActivation, changeCustomerActivation,
changeCustomerArchive, changeCustomerArchive,
@@ -189,6 +217,7 @@ import {
import { buildCustomerColumns } from '@/pages/menuPages/customers/base/hooks/customerColumns' import { buildCustomerColumns } from '@/pages/menuPages/customers/base/hooks/customerColumns'
import CustomerMobileCardList from '@/pages/menuPages/customers/base/components/CustomerMobileCardList.vue' import CustomerMobileCardList from '@/pages/menuPages/customers/base/components/CustomerMobileCardList.vue'
import CustomerDirectSmsModal from '@/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue' import CustomerDirectSmsModal from '@/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue'
import CustomerAccountingCodeModal from '@/pages/menuPages/customers/base/components/CustomerAccountingCodeModal.vue'
import Icon from '@core/components/BaseIcon/src/Icon.vue' import Icon from '@core/components/BaseIcon/src/Icon.vue'
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable' import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
import type { TableInput } from '@core/type/baseTable' import type { TableInput } from '@core/type/baseTable'
@@ -201,10 +230,12 @@ import PargarRetryModal from '@/components/PargarRetryModal.vue'
import { import {
tahesabEnabledSetting, tahesabEnabledSetting,
pargarEnabledSetting, pargarEnabledSetting,
accountingProviderSetting,
customerDirectSmsEnabledSetting, customerDirectSmsEnabledSetting,
initAccountingSettingsFromApi, initAccountingSettingsFromApi,
initWalletSettingsFromApi initWalletSettingsFromApi
} from '@/utils/config' } from '@/utils/config'
import { displayAccountingCode, isValidAccountingCode } from '@/utils/accountingCode'
const [submit, reload] = useBaseTable() const [submit, reload] = useBaseTable()
const emits = defineEmits(['delete', 'edit', 'add', 'info', 'chargeWallet']) const emits = defineEmits(['delete', 'edit', 'add', 'info', 'chargeWallet'])
@@ -219,16 +250,43 @@ const pargarRegisterVisible = ref(false)
const pargarCustomer = ref<Customer>() const pargarCustomer = ref<Customer>()
const pargarEnabled = pargarEnabledSetting const pargarEnabled = pargarEnabledSetting
const customerDirectSmsEnabled = customerDirectSmsEnabledSetting const customerDirectSmsEnabled = customerDirectSmsEnabledSetting
const accountingCodeVisible = ref(false)
const accountingCodeCustomer = ref<Customer | null>(null)
const accountingEnabled = computed(() => {
const provider = accountingProviderSetting.value
return provider === 'pargar' || provider === 'tahesab'
})
function accountingCodeOf(record: Customer) {
const provider = accountingProviderSetting.value
if (provider === 'pargar') return displayAccountingCode(record.pargar_account_id)
if (provider === 'tahesab') return displayAccountingCode(record.moshtari_code)
return '-'
}
function openAccountingCodeEdit(user: Customer) {
accountingCodeCustomer.value = user
accountingCodeVisible.value = true
}
const directSmsVisible = ref(false) const directSmsVisible = ref(false)
const directSmsCustomer = ref<Customer | null>(null) const directSmsCustomer = ref<Customer | null>(null)
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(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
})
}
const tblProps = ref<any>({ const tblProps = ref<any>({
columns: buildCustomerColumns(), columns: buildCustomerColumns([], { accountingProvider: accountingProviderSetting.value }),
api: async (params: TableInput) => { api: async (params: TableInput) => {
await initAccountingSettingsFromApi()
const data = await getCustomerTable(params) const data = await getCustomerTable(params)
const metals = data?.catalogMetals ?? [] const metals = data?.catalogMetals ?? []
tblProps.value.columns = buildCustomerColumns(metals) rebuildColumns(metals)
return data return data
} }
}) })
@@ -5,10 +5,34 @@ import { usePrice } from '@core/utils/usePrice'
import { getCashBalance } from '@/components/mobile/utils/wallet' import { getCashBalance } from '@/components/mobile/utils/wallet'
import { customerWalletMetalBalanceTitle } from '@/utils/adminMetalLabels' import { customerWalletMetalBalanceTitle } from '@/utils/adminMetalLabels'
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight' import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
import type { AccountingProvider } from '@/utils/config'
export type CustomerMetalColumn = { metal_id: number; label: string; display_unit?: string } export type CustomerMetalColumn = { metal_id: number; label: string; display_unit?: string }
export function buildCustomerColumns(metals: CustomerMetalColumn[] = []): BaseTableColumn[] { export type CustomerColumnOptions = {
accountingProvider?: AccountingProvider
}
function accountingCodeColumn(provider: AccountingProvider | undefined): BaseTableColumn | null {
if (provider === 'pargar' || provider === 'tahesab') {
return {
title: 'کد حسابداری',
dataIndex: provider === 'pargar' ? 'pargar_account_id' : 'moshtari_code',
key: 'accounting_code',
type: 'Text',
filterable: false,
slot: 'accountingCode'
}
}
return null
}
export function buildCustomerColumns(
metals: CustomerMetalColumn[] = [],
options: CustomerColumnOptions = {}
): BaseTableColumn[] {
const accountingColumn = accountingCodeColumn(options.accountingProvider)
const baseColumns: BaseTableColumn[] = [ const baseColumns: BaseTableColumn[] = [
{ {
title: 'ردیف', title: 'ردیف',
@@ -26,6 +50,7 @@ export function buildCustomerColumns(metals: CustomerMetalColumn[] = []): BaseTa
key: 'id', key: 'id',
type: 'Number' type: 'Number'
}, },
...(accountingColumn ? [accountingColumn] : []),
{ {
title: 'نام و نام خانوادگی', title: 'نام و نام خانوادگی',
dataIndex: 'name', dataIndex: 'name',
+24 -4
View File
@@ -153,6 +153,11 @@
<div class="grid min-w-0 grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"> <div class="grid min-w-0 grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
<BasicInfoItem title="کد کاربری" :value="userInfo?.id" /> <BasicInfoItem title="کد کاربری" :value="userInfo?.id" />
<BasicInfoItem
v-if="accountingCodeVisible"
title="کد حسابداری"
:value="accountingCodeValue"
/>
<BasicInfoItem <BasicInfoItem
title="تاریخ تولد" title="تاریخ تولد"
:value="userInfo?.birth_date ? toJalaliNoTime(userInfo?.birth_date) : '_'" :value="userInfo?.birth_date ? toJalaliNoTime(userInfo?.birth_date) : '_'"
@@ -246,7 +251,7 @@
// import BasicInfoModal from '@/components/BasicInfoModal' // import BasicInfoModal from '@/components/BasicInfoModal'
import { inputLenRules, inputRules } from '@/utils/inputRules' import { inputLenRules, inputRules } from '@/utils/inputRules'
import { useStaticApiListsStore } from '@/store/staticApisStore' import { useStaticApiListsStore } from '@/store/staticApisStore'
import { ref, watch, computed } from 'vue' import { ref, watch, computed, onMounted } from 'vue'
import tbl from './components/table.vue' import tbl from './components/table.vue'
import type { FormInstance } from 'ant-design-vue' import type { FormInstance } from 'ant-design-vue'
import BasicInfoModal from '@/components/BasicInfoModal/index.vue' import BasicInfoModal from '@/components/BasicInfoModal/index.vue'
@@ -277,6 +282,11 @@ import type { ChargeTarget } from '@/model/wallet'
import { metalLabel } from '@/utils/adminMetalLabels' import { metalLabel } from '@/utils/adminMetalLabels'
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy' import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue' import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
import {
accountingProviderSetting,
initAccountingSettingsFromApi
} from '@/utils/config'
import { displayAccountingCode } from '@/utils/accountingCode'
const fnameHelp: DashboardHelp = { const fnameHelp: DashboardHelp = {
description: 'نام کوچک مشتری که در اپ و پنل نمایش داده می‌شود.', description: 'نام کوچک مشتری که در اپ و پنل نمایش داده می‌شود.',
@@ -511,9 +521,7 @@ const deleteItem = (id: number) => confirmDeleteItem('کاربر', id, deleteCus
//define selects //define selects
onMounted(async () => { onMounted(async () => {
loading.value = true loading.value = true
// await getResponsible() await initAccountingSettingsFromApi()
// await getQC()
// await getManager()
loading.value = false loading.value = false
}) })
@@ -533,6 +541,18 @@ async function loadInfoChargeTargets() {
} }
const userInfo = ref<Customer>() const userInfo = ref<Customer>()
const accountingCodeVisible = computed(() => {
const provider = accountingProviderSetting.value
return provider === 'pargar' || provider === 'tahesab'
})
const accountingCodeValue = computed(() => {
const provider = accountingProviderSetting.value
if (provider === 'pargar') return displayAccountingCode(userInfo.value?.pargar_account_id)
if (provider === 'tahesab') return displayAccountingCode(userInfo.value?.moshtari_code)
return '-'
})
//submit user //submit user
async function ok() { async function ok() {
formRef.value?.validateFields().then(async () => { formRef.value?.validateFields().then(async () => {
@@ -18,6 +18,7 @@
:locale="customLocale" :locale="customLocale"
placeholder="انتخاب زمان" placeholder="انتخاب زمان"
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')" :defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
@change="onActivationTimeChange"
/> />
</div> </div>
<div class="flex items-center gap-1.5 md:mr-4"> <div class="flex items-center gap-1.5 md:mr-4">
@@ -29,9 +30,9 @@
:locale="customLocale" :locale="customLocale"
placeholder="انتخاب زمان" placeholder="انتخاب زمان"
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')" :defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
@change="onActivationTimeChange"
/> />
</div> </div>
<BaseButton class="md:mr-2" @click="setTimeActive(startTime, endTime)">ثبت</BaseButton>
</div> </div>
</div> </div>
@@ -152,8 +153,8 @@ dayjs.extend(customParseFormat)
const productActivationHelp: DashboardHelp = { const productActivationHelp: DashboardHelp = {
description: description:
'ساعت شروع و پایان را یک‌جا روی همهٔ محصولات ثبت می‌کند (فیلدهای auto_open_at و auto_close_at در دیتابیس).\n\n' + 'ساعت شروع و پایان را یک‌جا روی همهٔ محصولات اعمال می‌کند (فیلدهای auto_open_at و auto_close_at در دیتابیس). با تغییر هر ساعت، بلافاصله ذخیره می‌شود.\n\n' +
'بعد از «ثبت» چه می‌شود؟\n' + 'بعد از تغییر چه می‌شود؟\n' +
'• خارج از این بازه، نمایش محصول در لیست اپ (فیلد show) به‌روز می‌شود — مشتری آن محصول را در فهرست نمی‌بیند یا غیرفعال می‌ماند.\n' + '• خارج از این بازه، نمایش محصول در لیست اپ (فیلد show) به‌روز می‌شود — مشتری آن محصول را در فهرست نمی‌بیند یا غیرفعال می‌ماند.\n' +
'• مظنه و قیمت صفحهٔ اصلی برای مشتری همچنان نمایش داده می‌شود.\n' + '• مظنه و قیمت صفحهٔ اصلی برای مشتری همچنان نمایش داده می‌شود.\n' +
'• روشن/خاموش بودن خرید و فروش هر محصول جدا است و فقط با آیکون‌های جدول پایین تغییر می‌کند؛ این ساعت به‌تنهایی open_buy و open_sell را عوض نمی‌کند.\n' + '• روشن/خاموش بودن خرید و فروش هر محصول جدا است و فقط با آیکون‌های جدول پایین تغییر می‌کند؛ این ساعت به‌تنهایی open_buy و open_sell را عوض نمی‌کند.\n' +
@@ -256,6 +257,11 @@ async function archiveProductById(id: number) {
} }
} }
async function onActivationTimeChange() {
if (!startTime.value || !endTime.value) return
await setTimeActive(startTime.value, endTime.value)
}
async function setTimeActive(start: dayjs.Dayjs | string, end: dayjs.Dayjs | string) { async function setTimeActive(start: dayjs.Dayjs | string, end: dayjs.Dayjs | string) {
try { try {
const startTimeFormatted = dayjs(start).format('HH:mm:ss') const startTimeFormatted = dayjs(start).format('HH:mm:ss')
+2 -1
View File
@@ -26,7 +26,7 @@ import { useRouterStore } from '@core/stores/router'
import { useUserStore } from '@/store/userStore' import { useUserStore } from '@/store/userStore'
import { isDeployAdmin } from '@/utils/deployAdmin' import { isDeployAdmin } from '@/utils/deployAdmin'
import { hasSmsPanelAccess, isSmsBillingEnabled } from '@/utils/smsPanelAccess' import { hasSmsPanelAccess, isSmsBillingEnabled } from '@/utils/smsPanelAccess'
import { bannerEnabledSetting } from '@/utils/config' import { bannerEnabledSetting, trackingSystemsEnabledSetting } from '@/utils/config'
import { get } from 'lodash' import { get } from 'lodash'
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
@@ -39,6 +39,7 @@ const menu = computed(() => {
return children return children
.filter((child) => !child.meta?.hideMenu) .filter((child) => !child.meta?.hideMenu)
.filter((child) => child.name !== 'banners' || bannerEnabledSetting.value) .filter((child) => child.name !== 'banners' || bannerEnabledSetting.value)
.filter((child) => child.name !== 'captureSitesMain' || trackingSystemsEnabledSetting.value)
.filter( .filter(
(child) => (child) =>
!child.meta?.requiresSmsPanelAccess || !child.meta?.requiresSmsPanelAccess ||
+6 -1
View File
@@ -2,7 +2,7 @@ import type { RouteRecordRaw } from 'vue-router'
import { isDeployAdmin } from '@/utils/deployAdmin' import { isDeployAdmin } from '@/utils/deployAdmin'
import { hasSmsPanelAccess } from '@/utils/smsPanelAccess' import { hasSmsPanelAccess } from '@/utils/smsPanelAccess'
import { useUserStore } from '@/store/userStore' import { useUserStore } from '@/store/userStore'
import { bannerEnabledSetting } from '@/utils/config' import { bannerEnabledSetting, trackingSystemsEnabledSetting } from '@/utils/config'
const Setting: RouteRecordRaw = { const Setting: RouteRecordRaw = {
name: 'settings', name: 'settings',
@@ -52,6 +52,11 @@ const Setting: RouteRecordRaw = {
icon: 'vuesax-linear:global', icon: 'vuesax-linear:global',
title: 'سایت‌های ری‌گیری', title: 'سایت‌های ری‌گیری',
order: 2 order: 2
},
beforeEnter() {
if (!trackingSystemsEnabledSetting.value) {
return { name: 'configuration' }
}
} }
}, },
{ {
+17
View File
@@ -0,0 +1,17 @@
/** کدهای نامعتبر که پرگار/ته‌حساب گاهی به‌جای کد واقعی می‌فرستند */
const INVALID_ACCOUNTING_CODES = new Set(['', '0', '-1', 'null', 'undefined'])
export function normalizeAccountingCode(value: unknown): string | null {
if (value === null || value === undefined) return null
const trimmed = String(value).trim()
if (INVALID_ACCOUNTING_CODES.has(trimmed.toLowerCase())) return null
return trimmed
}
export function isValidAccountingCode(value: unknown): boolean {
return normalizeAccountingCode(value) !== null
}
export function displayAccountingCode(value: unknown): string {
return normalizeAccountingCode(value) ?? '-'
}
+16
View File
@@ -28,6 +28,9 @@ let settingsBootstrapped = false
/** reactive — برای فیلتر منوی «بنرها» در پیکربندی */ /** reactive — برای فیلتر منوی «بنرها» در پیکربندی */
export const bannerEnabledSetting = ref(false) export const bannerEnabledSetting = ref(false)
/** reactive — برای فیلتر منوی «سایت‌های ری‌گیری» در پیکربندی */
export const trackingSystemsEnabledSetting = ref(false)
/** reactive — نمایش UI ته‌حساب در حسابداری و ... */ /** reactive — نمایش UI ته‌حساب در حسابداری و ... */
export const tahesabEnabledSetting = ref(false) export const tahesabEnabledSetting = ref(false)
@@ -90,6 +93,10 @@ export function setBannerEnabledFromSettings(value: boolean): void {
bannerEnabledSetting.value = value bannerEnabledSetting.value = value
} }
export function setTrackingSystemsEnabledFromSettings(value: boolean): void {
trackingSystemsEnabledSetting.value = value
}
export function setTahesabEnabledFromSettings(value: boolean): void { export function setTahesabEnabledFromSettings(value: boolean): void {
tahesabEnabledFromSettings = value tahesabEnabledFromSettings = value
tahesabEnabledSetting.value = value tahesabEnabledSetting.value = value
@@ -110,6 +117,13 @@ export function applyBannerEnabledFromRows(rows: Array<{ key?: string; value?: s
setBannerEnabledFromSettings(row ? isSwitchOn(row.value) : false) setBannerEnabledFromSettings(row ? isSwitchOn(row.value) : false)
} }
export function applyTrackingSystemsEnabledFromRows(
rows: Array<{ key?: string; value?: string | null }>
) {
const row = rows.find((item) => item.key === 'tracking_systems_enabled')
setTrackingSystemsEnabledFromSettings(row ? isSwitchOn(row.value) : false)
}
export function applyCustomerDirectSmsFromRows(rows: Array<{ key?: string; value?: string | null }>) { export function applyCustomerDirectSmsFromRows(rows: Array<{ key?: string; value?: string | null }>) {
const row = rows.find((item) => item.key === 'customer_direct_sms') const row = rows.find((item) => item.key === 'customer_direct_sms')
setCustomerDirectSmsEnabledFromSettings(row ? isSwitchOn(row.value) : false) setCustomerDirectSmsEnabledFromSettings(row ? isSwitchOn(row.value) : false)
@@ -129,6 +143,7 @@ async function bootstrapSettingsFromApi(): Promise<void> {
storeModeFromSettings = isStoreRow ? isSwitchOn(isStoreRow.value) : false storeModeFromSettings = isStoreRow ? isSwitchOn(isStoreRow.value) : false
applyAccountingProviderFromRows(list) applyAccountingProviderFromRows(list)
applyBannerEnabledFromRows(list) applyBannerEnabledFromRows(list)
applyTrackingSystemsEnabledFromRows(list)
applyCustomerDirectSmsFromRows(list) applyCustomerDirectSmsFromRows(list)
setCachedMainMetal(mainMetalRow?.value) setCachedMainMetal(mainMetalRow?.value)
const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet') const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet')
@@ -136,6 +151,7 @@ async function bootstrapSettingsFromApi(): Promise<void> {
} catch { } catch {
storeModeFromSettings = false storeModeFromSettings = false
setBannerEnabledFromSettings(false) setBannerEnabledFromSettings(false)
setTrackingSystemsEnabledFromSettings(false)
setCustomerDirectSmsEnabledFromSettings(false) setCustomerDirectSmsEnabledFromSettings(false)
applyAccountingProviderFromRows([]) applyAccountingProviderFromRows([])
setCachedMainMetal(null) setCachedMainMetal(null)