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() {