From 5ea97e873a05a12e3f0345c8b41aca565696def2 Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 20 Jul 2026 21:28:35 +0330 Subject: [PATCH] added bannig when amount is under 0 --- src/components/BasicActionSheet.vue | 4 +- src/config-loader.js | 270 +++++++++++++++--- .../menuPages/configuration/settingGroups.ts | 4 +- .../menuPages/configuration/settingTypes.ts | 5 + src/pages/menuPages/products/base/index.vue | 132 +++++---- 5 files changed, 319 insertions(+), 96 deletions(-) diff --git a/src/components/BasicActionSheet.vue b/src/components/BasicActionSheet.vue index 01018bd..5222c3b 100644 --- a/src/components/BasicActionSheet.vue +++ b/src/components/BasicActionSheet.vue @@ -64,7 +64,9 @@

{{ title }}

- + + +
url !== lastSuccessUrl)); } - function sortConfigUrls(urls) { - const lastSuccessUrl = localStorage.getItem(CONFIG_SUCCESS_URL_KEY); - - return [...urls].sort((a, b) => { - if (a === lastSuccessUrl) return -1; - if (b === lastSuccessUrl) return 1; - return 0; - }); - } - function clearStaleSuccessUrl(key, url) { if (localStorage.getItem(key) === url) { localStorage.removeItem(key); } } - async function fetchJson(url, timeout = 3500) { + function normalizeVersion(value) { + if (value === null || value === undefined) return ''; + return String(value).trim(); + } + + function readUserVersion(user) { + if (!user || typeof user !== 'object' || Array.isArray(user)) return ''; + const version = user.version ?? user.value; + return version === undefined || version === null ? '' : String(version).trim(); + } + + function normalizeVersionResponse(response) { + if (typeof response === 'string' || typeof response === 'number') { + return String(response).trim(); + } + if (!response || typeof response !== 'object') return ''; + + if (typeof response.data === 'string' || typeof response.data === 'number') { + return String(response.data).trim(); + } + + const directVersion = + response.version ?? + response.value ?? + (response.data && typeof response.data === 'object' + ? response.data.version ?? response.data.value + : undefined); + + if (directVersion !== undefined && directVersion !== null) { + return String(directVersion).trim(); + } + + return readUserVersion(response.user); + } + + function readCachedAppConfig() { + try { + const raw = localStorage.getItem(CONFIG_CACHE_KEY); + if (!raw) return null; + const payload = JSON.parse(raw); + if (!payload || typeof payload !== 'object' || !payload.config) return null; + return payload.config; + } catch (error) { + console.warn('Failed to read cached get_user:', error); + localStorage.removeItem(CONFIG_CACHE_KEY); + return null; + } + } + + function writeCachedAppConfig(config) { + try { + localStorage.setItem( + CONFIG_CACHE_KEY, + JSON.stringify({ + cached_at: Date.now(), + config: config + }) + ); + } catch (error) { + console.warn('Failed to write get_user cache:', error); + } + } + + function getStoredAppVersion() { + return localStorage.getItem(VERSION_STORAGE_KEY); + } + + function writeStoredAppVersion(version) { + localStorage.setItem(VERSION_STORAGE_KEY, normalizeVersion(version)); + } + + function shouldUpdateForVersion(serverVersion) { + const normalized = normalizeVersion(serverVersion); + if (!normalized) return false; + + const stored = getStoredAppVersion(); + if (!stored) { + writeStoredAppVersion(normalized); + return false; + } + + return stored !== normalized; + } + + function canStartVersionUpdate(serverVersion) { + const version = normalizeVersion(serverVersion); + if (!version) return false; + + if (sessionStorage.getItem(VERSION_LAST_RELOAD_KEY) === version) { + return false; + } + + const rawLock = sessionStorage.getItem(VERSION_UPDATE_LOCK_KEY); + if (rawLock) { + try { + const lock = JSON.parse(rawLock); + const isSameVersion = lock.version === version; + const isFreshLock = Date.now() - lock.started_at < 60_000; + if (isSameVersion && isFreshLock) return false; + } catch { + sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY); + } + } + + return true; + } + + function startVersionUpdateLock(serverVersion) { + sessionStorage.setItem( + VERSION_UPDATE_LOCK_KEY, + JSON.stringify({ + version: normalizeVersion(serverVersion), + started_at: Date.now() + }) + ); + } + + function finishVersionUpdateBeforeReload(serverVersion) { + const version = normalizeVersion(serverVersion); + writeStoredAppVersion(version); + sessionStorage.setItem(VERSION_LAST_RELOAD_KEY, version); + sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY); + } + + function cancelVersionUpdateLock() { + sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY); + } + + function preserveAuthToken() { + return localStorage.getItem(AUTH_TOKEN_KEY); + } + + function restoreAuthToken(token) { + if (token) { + localStorage.setItem(AUTH_TOKEN_KEY, token); + } + } + + function clearBootstrapCachesKeepToken() { + const token = preserveAuthToken(); + localStorage.removeItem(CONFIG_CACHE_KEY); + localStorage.removeItem(CONFIG_SUCCESS_URL_KEY); + localStorage.removeItem(VERSION_SUCCESS_URL_KEY); + restoreAuthToken(token); + } + + function hardReloadApp() { + const refreshUrl = + window.location.origin + + window.location.pathname + + '?__app_refresh=' + + Date.now() + + (window.location.hash || ''); + window.location.replace(refreshUrl); + } + + async function fetchJson(url, timeout) { const controller = new AbortController(); const timeoutId = window.setTimeout(() => controller.abort(), timeout); @@ -105,8 +252,9 @@ } } - async function loadVersion(rounds = VERSION_RETRY_ROUNDS) { - const urls = sortVersionUrls(getVersionUrls()); + async function loadVersion(rounds) { + rounds = rounds || VERSION_RETRY_ROUNDS; + const urls = sortUrls(getVersionUrls(), VERSION_SUCCESS_URL_KEY); for (let round = 1; round <= rounds; round++) { for (let index = 0; index < urls.length; index++) { @@ -114,10 +262,14 @@ const timeout = index === urls.length - 1 ? 6000 : 3500; try { - await fetchJson(url, timeout); + const data = await fetchJson(url, timeout); + const version = normalizeVersionResponse(data); + if (!version) { + throw new Error('Version response is empty'); + } localStorage.setItem(VERSION_SUCCESS_URL_KEY, url); - console.log('Version loaded from:', url); - return; + console.log('Version loaded from:', url, version); + return version; } catch (error) { clearStaleSuccessUrl(VERSION_SUCCESS_URL_KEY, url); console.warn('Version failed from', url, 'round:', round, error); @@ -126,10 +278,12 @@ } console.warn('Version check failed from all endpoints'); + return null; } - async function fetchConfig(rounds = CONFIG_RETRY_ROUNDS) { - const urls = sortConfigUrls(getConfigUrls()); + async function fetchConfig(rounds) { + rounds = rounds || CONFIG_RETRY_ROUNDS; + const urls = sortUrls(getConfigUrls(), CONFIG_SUCCESS_URL_KEY); for (let round = 1; round <= rounds; round++) { for (let index = 0; index < urls.length; index++) { @@ -144,7 +298,8 @@ } localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url); - return { config, url }; + writeCachedAppConfig(config); + return { config: config, url: url }; } catch (error) { clearStaleSuccessUrl(CONFIG_SUCCESS_URL_KEY, url); console.warn('Config failed from', url, 'round:', round, error); @@ -156,14 +311,14 @@ } function applyConfig(config) { - window.__APP_CONFIG__ = config; + window.__APP_CONFIG__ = config || {}; window.__CONFIG_READY__ = true; - if (config?.user?.name_fa) { + if (config && config.user && config.user.name_fa) { document.title = 'پنل مدیریت ' + '-' + config.user.name_fa; } - if (config?.user?.avatar) { + if (config && config.user && config.user.avatar) { const faviconUrl = config.user.avatar; const existingIcons = document.querySelectorAll('link[rel*="icon"]'); existingIcons.forEach((icon) => icon.remove()); @@ -195,19 +350,59 @@ loadMainScript(); } + async function applyVersionUpdate(serverVersion) { + startVersionUpdateLock(serverVersion); + + try { + const { config } = await fetchConfig(); + clearBootstrapCachesKeepToken(); + writeCachedAppConfig(config); + applyConfig(config); + finishVersionUpdateBeforeReload(serverVersion); + console.log('App version changed — hard refresh', serverVersion); + hardReloadApp(); + return true; + } catch (error) { + console.error('Version update failed:', error); + cancelVersionUpdateLock(); + return false; + } + } + async function loadConfig() { try { - await loadVersion(); + // ۱) کش get_user — اگر بود سریع اعمال شود + const cachedConfig = readCachedAppConfig(); + if (cachedConfig) { + applyConfig(cachedConfig); + console.log('Applied cached get_user'); + } + // ۲) get_version — اگر تغییر کرده hard refresh + const remoteVersion = await loadVersion(); + if (remoteVersion && shouldUpdateForVersion(remoteVersion) && canStartVersionUpdate(remoteVersion)) { + const updating = await applyVersionUpdate(remoteVersion); + if (updating) { + return; + } + } + + // ۳) تازه کردن get_user و کش const { config, url } = await fetchConfig(); applyConfig(config); - console.log('Config loaded successfully from', url, config); finishConfigLoad(config); } catch (error) { console.warn('Config loader: Could not fetch config from any endpoint'); - console.warn('Error details:', error.message); - console.warn('App will continue with empty config.'); + console.warn('Error details:', error && error.message ? error.message : error); + console.warn('App will continue with empty/cached config.'); + + const cachedConfig = readCachedAppConfig(); + if (cachedConfig) { + applyConfig(cachedConfig); + finishConfigLoad(cachedConfig, error); + return; + } window.__APP_CONFIG__ = {}; window.__CONFIG_READY__ = true; @@ -229,12 +424,13 @@ configTimeoutId = setTimeout(function() { if (!mainScriptLoaded) { - console.warn('Config loader: Timeout reached. Loading main.ts with empty config.'); - window.__APP_CONFIG__ = window.__APP_CONFIG__ || {}; + console.warn('Config loader: Timeout reached. Loading main.ts with empty/cached config.'); + const cachedConfig = readCachedAppConfig(); + window.__APP_CONFIG__ = cachedConfig || window.__APP_CONFIG__ || {}; window.__CONFIG_READY__ = true; if (configResolve) { - configResolve({}); + configResolve(window.__APP_CONFIG__); } loadMainScript(); diff --git a/src/pages/menuPages/configuration/settingGroups.ts b/src/pages/menuPages/configuration/settingGroups.ts index ee3fcab..b9c8531 100644 --- a/src/pages/menuPages/configuration/settingGroups.ts +++ b/src/pages/menuPages/configuration/settingGroups.ts @@ -102,7 +102,7 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [ id: 'features', title: 'قابلیت‌های سیستم', icon: 'vuesax-linear:setting-2', - match: (k) => /^(product_|main_wallet_products|profit_gold)/.test(k) + match: (k) => /^(product_|main_wallet_products|require_zero_main_gold_before_other_buy|profit_gold)/.test(k) }, { id: 'misc', @@ -119,7 +119,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [ title: 'عمومی و برند', icon: 'vuesax-linear:home-2', match: (k) => - /^(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( + /^(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|main_wallet_products|require_zero_main_gold_before_other_buy)/.test( k ) }, diff --git a/src/pages/menuPages/configuration/settingTypes.ts b/src/pages/menuPages/configuration/settingTypes.ts index f3bc6f5..7e369a9 100644 --- a/src/pages/menuPages/configuration/settingTypes.ts +++ b/src/pages/menuPages/configuration/settingTypes.ts @@ -231,6 +231,7 @@ const KEY_TYPE_FALLBACK: Record = { text_about: 'longtext', fee_service_charge_percentage: 'number', fee_deduct_from_gold: 'switch', + require_zero_main_gold_before_other_buy: 'switch', fee_single: 'price', buy_fee: 'price', sell_fee: 'price', @@ -550,6 +551,8 @@ const KEY_HELP_FALLBACK: Record = { 'درصد کارمزد هر معامله از مبلغ سفارش (اعشاری؛ مثلاً ۰٫۰۰۵ یعنی نیم در هزار).', fee_deduct_from_gold: 'این کلید دیگر روی محاسبه اثری ندارد. مبنای کارمزد اکنون فیلدی است که مشتری وارد می‌کند: اگر «مبلغ ریالی» را وارد کند (مثلاً دقیقاً ۱۰۰ میلیون ریال) همان مبلغ معامله می‌شود و کارمزد از طلا کسر می‌شود؛ اگر «وزن» را وارد کند (مثلاً دقیقاً ۲ گرم) همان وزن معامله می‌شود و کارمزد از ریال کسر می‌شود. این قانون برای خرید و فروش یکسان است.', + require_zero_main_gold_before_other_buy: + 'روشن: اگر مانده طلای کیف اصلی صفر یا زیر صفر باشد (بدهی)، مشتری ابتدا باید آبشده/مانده کیف اصلی را صفر کند و بعد می‌تواند محصول دیگر بخرد. خاموش: بدون این محدودیت. معامله خودِ محصولات کیف اصلی همیشه مجاز است.', fee_free: 'تعداد اولین سفارش‌های هر کاربر که از کارمزد معاف می‌شوند. صفر یعنی غیرفعال.', min_withdraw: @@ -778,6 +781,7 @@ export const KEY_LABEL_FALLBACK: Record = { sell_fee: 'کسر قیمت فروش', fee_service_charge_percentage: 'درصد کارمزد معامله', fee_deduct_from_gold: 'اعمال مبلغ دقیق در معامله مشتری', + require_zero_main_gold_before_other_buy: 'الزام صفر بودن کیف اصلی قبل از خرید محصول دیگر', fee_free: 'معافیت کارمزد سفارش اول', min_withdraw: 'حداقل مبلغ برداشت', min_deposit: 'حداقل مبلغ واریز کیف پول', @@ -860,6 +864,7 @@ export const SETTING_LOCKED_KEYS = new Set([ 'sms_panel', 'logo_login', 'logo_register', 'title_pwa', 'title_fav', 'main_product', 'main_wallet_products', 'metal_type', 'main_metal', + 'require_zero_main_gold_before_other_buy', 'montify_name', 'montzar_base_url', 'montzar_closed', 'montzar_diff', 'montzar_last_product_log', 'montzar_live_price', 'montzar_mazaneh', diff --git a/src/pages/menuPages/products/base/index.vue b/src/pages/menuPages/products/base/index.vue index 7b33039..c9d1dad 100644 --- a/src/pages/menuPages/products/base/index.vue +++ b/src/pages/menuPages/products/base/index.vue @@ -293,73 +293,88 @@ async function setProductIndexImage(record: Record, productId?: num } async function edit(record: Product) { - await loadCategorySelectItems() - addUserVisible.value = true + submitError.value = '' editId = record.id - let productData = cloneDeep(record) as Record + addUserVisible.value = true + loading.value = true try { - const detail = unwrapApiRecord(await getCustomerById(record.id)) - if (detail) productData = { ...productData, ...detail } - } catch { - /* use table row data */ - } + let productData = cloneDeep(record) as Record - methodOption.value = productData.app_price - ? '3' - : (productData.reading_method ? '2' : '1') - meltedPriceOption.value = resolveMeltedPriceSource(productData) - if (usesMeltedPriceEditor(productData)) { - methodOption.value = '1' - if ( - !isMeltedTradeGroup(productData.group_type) - && !productData.reading_method - && Number(productData.direct_price) !== 1 - && Number(productData.price) > 0 - ) { - meltedPriceOption.value = 'fixed' + const [, detailResult] = await Promise.all([ + loadCategorySelectItems(), + getCustomerById(record.id) + .then((res) => unwrapApiRecord(res)) + .catch(() => null), + refreshLivePrices(), + ]) + + if (detailResult) { + productData = { ...productData, ...detailResult } } + + methodOption.value = productData.app_price + ? '3' + : (productData.reading_method ? '2' : '1') + meltedPriceOption.value = resolveMeltedPriceSource(productData) + if (usesMeltedPriceEditor(productData)) { + methodOption.value = '1' + if ( + !isMeltedTradeGroup(productData.group_type) + && !productData.reading_method + && Number(productData.direct_price) !== 1 + && Number(productData.price) > 0 + ) { + meltedPriceOption.value = 'fixed' + } + } + weightOption.value = Number(productData.amount) > 0 ? '1' : '2' + if (weightOption.value === '2' && !isMeltedTradeGroup(productData.group_type)) { + productData.unit = undefined + } + + const start = productData.auto_open_at + ? dayjs(productData.auto_open_at, 'HH:mm:ss') + : dayjs('00:00:00', 'HH:mm:ss') + const close = productData.auto_close_at + ? dayjs(productData.auto_close_at, 'HH:mm:ss') + : dayjs('00:00:00', 'HH:mm:ss') + + form.value = normalizeProductOrderLimitForm({ + ...productData, + show_in_assets: productData.show_in_assets ?? false, + trade_by_quantity: productData.trade_by_quantity ?? false, + app_price: Boolean(productData.app_price), + override_trading_hours: productData.override_trading_hours ?? false, + has_physical_delivery: productData.has_physical_delivery ?? false, + min_physical_delivery_quantity: + productData.min_physical_delivery_quantity ?? + (productData as { min_physical_delivery_grams?: number | null }).min_physical_delivery_grams ?? + undefined, + cash_fee_buy: parseCashFeeValue(productData.cash_fee_buy), + cash_fee_sell: parseCashFeeValue(productData.cash_fee_sell), + cash_fee_buy_is_percent: normalizeCashFeeFlags(productData.cash_fee_buy_is_percent), + cash_fee_sell_is_percent: normalizeCashFeeFlags(productData.cash_fee_sell_is_percent), + price_offset_percent: productData.price_offset_percent ?? 0, + direct_price: productData.direct_price ?? 0, + auto_open_at: start.isValid() ? start : dayjs('00:00:00', 'HH:mm:ss'), + auto_close_at: close.isValid() ? close : dayjs('00:00:00', 'HH:mm:ss') + }) + + await setProductIndexImage(productData, record.id) + } catch (err) { + console.error(err) + message.error('خطا در دریافت اطلاعات محصول') + } finally { + loading.value = false } - weightOption.value = Number(productData.amount) > 0 ? '1' : '2' - if (weightOption.value === '2' && !isMeltedTradeGroup(productData.group_type)) { - productData.unit = undefined - } - - const start = productData.auto_open_at - ? dayjs(productData.auto_open_at, 'HH:mm:ss') - : dayjs('00:00:00', 'HH:mm:ss') - const close = productData.auto_close_at - ? dayjs(productData.auto_close_at, 'HH:mm:ss') - : dayjs('00:00:00', 'HH:mm:ss') - - form.value = normalizeProductOrderLimitForm({ - ...productData, - show_in_assets: productData.show_in_assets ?? false, - trade_by_quantity: productData.trade_by_quantity ?? false, - app_price: Boolean(productData.app_price), - override_trading_hours: productData.override_trading_hours ?? false, - has_physical_delivery: productData.has_physical_delivery ?? false, - min_physical_delivery_quantity: - productData.min_physical_delivery_quantity ?? - (productData as { min_physical_delivery_grams?: number | null }).min_physical_delivery_grams ?? - undefined, - cash_fee_buy: parseCashFeeValue(productData.cash_fee_buy), - cash_fee_sell: parseCashFeeValue(productData.cash_fee_sell), - cash_fee_buy_is_percent: normalizeCashFeeFlags(productData.cash_fee_buy_is_percent), - cash_fee_sell_is_percent: normalizeCashFeeFlags(productData.cash_fee_sell_is_percent), - price_offset_percent: productData.price_offset_percent ?? 0, - direct_price: productData.direct_price ?? 0, - auto_open_at: start.isValid() ? start : dayjs('00:00:00', 'HH:mm:ss'), - auto_close_at: close.isValid() ? close : dayjs('00:00:00', 'HH:mm:ss') - }) - - await setProductIndexImage(productData, record.id) } function closeProductModal() { addUserVisible.value = false editId = undefined submitError.value = '' + loading.value = false } async function add() { @@ -367,7 +382,12 @@ async function add() { submitError.value = '' addUserVisible.value = true resetData() - await loadCategorySelectItems() + loading.value = true + try { + await Promise.all([loadCategorySelectItems(), refreshLivePrices()]) + } finally { + loading.value = false + } } function resetData() {