added bannig when amount is under 0

This commit is contained in:
2026-07-20 21:28:35 +03:30
parent 2b58db3a91
commit 5ea97e873a
5 changed files with 319 additions and 96 deletions
+2
View File
@@ -64,7 +64,9 @@
<h3 class="basic-action-sheet__title">{{ title }}</h3> <h3 class="basic-action-sheet__title">{{ title }}</h3>
</div> </div>
<div class="basic-action-sheet__content"> <div class="basic-action-sheet__content">
<a-spin :spinning="loading">
<slot /> <slot />
</a-spin>
</div> </div>
<div <div
v-if="showButton && (showCancel || showOk)" v-if="showButton && (showCancel || showOk)"
+233 -37
View File
@@ -13,10 +13,15 @@
const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL'; const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL';
const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL'; const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL';
const CONFIG_CACHE_KEY = 'APP_CONFIG_CACHE';
const VERSION_STORAGE_KEY = 'version';
const VERSION_UPDATE_LOCK_KEY = 'version_update_lock';
const VERSION_LAST_RELOAD_KEY = 'version_last_reload';
const AUTH_TOKEN_KEY = 'peransa_token';
const VERSION_RETRY_ROUNDS = 5; const VERSION_RETRY_ROUNDS = 5;
const CONFIG_RETRY_ROUNDS = 5; const CONFIG_RETRY_ROUNDS = 5;
const CONFIG_TIMEOUT = 12000; const CONFIG_TIMEOUT = 12000;
let configTimeoutId; let configTimeoutId;
let mainScriptLoaded = false; let mainScriptLoaded = false;
@@ -48,33 +53,175 @@
]; ];
} }
function sortVersionUrls(urls) { function sortUrls(urls, successKey) {
const lastSuccessUrl = localStorage.getItem(VERSION_SUCCESS_URL_KEY); const lastSuccessUrl = localStorage.getItem(successKey);
if (!lastSuccessUrl) return urls.slice();
if (!lastSuccessUrl) {
return urls.slice();
}
return [lastSuccessUrl].concat(urls.filter((url) => url !== lastSuccessUrl)); return [lastSuccessUrl].concat(urls.filter((url) => 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) { function clearStaleSuccessUrl(key, url) {
if (localStorage.getItem(key) === url) { if (localStorage.getItem(key) === url) {
localStorage.removeItem(key); 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 controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), timeout); const timeoutId = window.setTimeout(() => controller.abort(), timeout);
@@ -105,8 +252,9 @@
} }
} }
async function loadVersion(rounds = VERSION_RETRY_ROUNDS) { async function loadVersion(rounds) {
const urls = sortVersionUrls(getVersionUrls()); rounds = rounds || VERSION_RETRY_ROUNDS;
const urls = sortUrls(getVersionUrls(), VERSION_SUCCESS_URL_KEY);
for (let round = 1; round <= rounds; round++) { for (let round = 1; round <= rounds; round++) {
for (let index = 0; index < urls.length; index++) { for (let index = 0; index < urls.length; index++) {
@@ -114,10 +262,14 @@
const timeout = index === urls.length - 1 ? 6000 : 3500; const timeout = index === urls.length - 1 ? 6000 : 3500;
try { 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); localStorage.setItem(VERSION_SUCCESS_URL_KEY, url);
console.log('Version loaded from:', url); console.log('Version loaded from:', url, version);
return; return version;
} catch (error) { } catch (error) {
clearStaleSuccessUrl(VERSION_SUCCESS_URL_KEY, url); clearStaleSuccessUrl(VERSION_SUCCESS_URL_KEY, url);
console.warn('Version failed from', url, 'round:', round, error); console.warn('Version failed from', url, 'round:', round, error);
@@ -126,10 +278,12 @@
} }
console.warn('Version check failed from all endpoints'); console.warn('Version check failed from all endpoints');
return null;
} }
async function fetchConfig(rounds = CONFIG_RETRY_ROUNDS) { async function fetchConfig(rounds) {
const urls = sortConfigUrls(getConfigUrls()); rounds = rounds || CONFIG_RETRY_ROUNDS;
const urls = sortUrls(getConfigUrls(), CONFIG_SUCCESS_URL_KEY);
for (let round = 1; round <= rounds; round++) { for (let round = 1; round <= rounds; round++) {
for (let index = 0; index < urls.length; index++) { for (let index = 0; index < urls.length; index++) {
@@ -144,7 +298,8 @@
} }
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url); localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url);
return { config, url }; writeCachedAppConfig(config);
return { config: config, url: url };
} catch (error) { } catch (error) {
clearStaleSuccessUrl(CONFIG_SUCCESS_URL_KEY, url); clearStaleSuccessUrl(CONFIG_SUCCESS_URL_KEY, url);
console.warn('Config failed from', url, 'round:', round, error); console.warn('Config failed from', url, 'round:', round, error);
@@ -156,14 +311,14 @@
} }
function applyConfig(config) { function applyConfig(config) {
window.__APP_CONFIG__ = config; window.__APP_CONFIG__ = config || {};
window.__CONFIG_READY__ = true; window.__CONFIG_READY__ = true;
if (config?.user?.name_fa) { if (config && config.user && config.user.name_fa) {
document.title = 'پنل مدیریت ' + '-' + 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 faviconUrl = config.user.avatar;
const existingIcons = document.querySelectorAll('link[rel*="icon"]'); const existingIcons = document.querySelectorAll('link[rel*="icon"]');
existingIcons.forEach((icon) => icon.remove()); existingIcons.forEach((icon) => icon.remove());
@@ -195,19 +350,59 @@
loadMainScript(); 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() { async function loadConfig() {
try { 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(); const { config, url } = await fetchConfig();
applyConfig(config); applyConfig(config);
console.log('Config loaded successfully from', url, config); console.log('Config loaded successfully from', url, config);
finishConfigLoad(config); finishConfigLoad(config);
} catch (error) { } catch (error) {
console.warn('Config loader: Could not fetch config from any endpoint'); console.warn('Config loader: Could not fetch config from any endpoint');
console.warn('Error details:', error.message); console.warn('Error details:', error && error.message ? error.message : error);
console.warn('App will continue with empty config.'); console.warn('App will continue with empty/cached config.');
const cachedConfig = readCachedAppConfig();
if (cachedConfig) {
applyConfig(cachedConfig);
finishConfigLoad(cachedConfig, error);
return;
}
window.__APP_CONFIG__ = {}; window.__APP_CONFIG__ = {};
window.__CONFIG_READY__ = true; window.__CONFIG_READY__ = true;
@@ -229,12 +424,13 @@
configTimeoutId = setTimeout(function() { configTimeoutId = setTimeout(function() {
if (!mainScriptLoaded) { if (!mainScriptLoaded) {
console.warn('Config loader: Timeout reached. Loading main.ts with empty config.'); console.warn('Config loader: Timeout reached. Loading main.ts with empty/cached config.');
window.__APP_CONFIG__ = window.__APP_CONFIG__ || {}; const cachedConfig = readCachedAppConfig();
window.__APP_CONFIG__ = cachedConfig || window.__APP_CONFIG__ || {};
window.__CONFIG_READY__ = true; window.__CONFIG_READY__ = true;
if (configResolve) { if (configResolve) {
configResolve({}); configResolve(window.__APP_CONFIG__);
} }
loadMainScript(); loadMainScript();
@@ -102,7 +102,7 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
id: 'features', id: 'features',
title: 'قابلیت‌های سیستم', title: 'قابلیت‌های سیستم',
icon: 'vuesax-linear:setting-2', 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', id: 'misc',
@@ -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|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 k
) )
}, },
@@ -231,6 +231,7 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
text_about: 'longtext', text_about: 'longtext',
fee_service_charge_percentage: 'number', fee_service_charge_percentage: 'number',
fee_deduct_from_gold: 'switch', fee_deduct_from_gold: 'switch',
require_zero_main_gold_before_other_buy: 'switch',
fee_single: 'price', fee_single: 'price',
buy_fee: 'price', buy_fee: 'price',
sell_fee: 'price', sell_fee: 'price',
@@ -550,6 +551,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
'درصد کارمزد هر معامله از مبلغ سفارش (اعشاری؛ مثلاً ۰٫۰۰۵ یعنی نیم در هزار).', 'درصد کارمزد هر معامله از مبلغ سفارش (اعشاری؛ مثلاً ۰٫۰۰۵ یعنی نیم در هزار).',
fee_deduct_from_gold: fee_deduct_from_gold:
'این کلید دیگر روی محاسبه اثری ندارد. مبنای کارمزد اکنون فیلدی است که مشتری وارد می‌کند: اگر «مبلغ ریالی» را وارد کند (مثلاً دقیقاً ۱۰۰ میلیون ریال) همان مبلغ معامله می‌شود و کارمزد از طلا کسر می‌شود؛ اگر «وزن» را وارد کند (مثلاً دقیقاً ۲ گرم) همان وزن معامله می‌شود و کارمزد از ریال کسر می‌شود. این قانون برای خرید و فروش یکسان است.', 'این کلید دیگر روی محاسبه اثری ندارد. مبنای کارمزد اکنون فیلدی است که مشتری وارد می‌کند: اگر «مبلغ ریالی» را وارد کند (مثلاً دقیقاً ۱۰۰ میلیون ریال) همان مبلغ معامله می‌شود و کارمزد از طلا کسر می‌شود؛ اگر «وزن» را وارد کند (مثلاً دقیقاً ۲ گرم) همان وزن معامله می‌شود و کارمزد از ریال کسر می‌شود. این قانون برای خرید و فروش یکسان است.',
require_zero_main_gold_before_other_buy:
'روشن: اگر مانده طلای کیف اصلی صفر یا زیر صفر باشد (بدهی)، مشتری ابتدا باید آبشده/مانده کیف اصلی را صفر کند و بعد می‌تواند محصول دیگر بخرد. خاموش: بدون این محدودیت. معامله خودِ محصولات کیف اصلی همیشه مجاز است.',
fee_free: fee_free:
'تعداد اولین سفارش‌های هر کاربر که از کارمزد معاف می‌شوند. صفر یعنی غیرفعال.', 'تعداد اولین سفارش‌های هر کاربر که از کارمزد معاف می‌شوند. صفر یعنی غیرفعال.',
min_withdraw: min_withdraw:
@@ -778,6 +781,7 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
sell_fee: 'کسر قیمت فروش', sell_fee: 'کسر قیمت فروش',
fee_service_charge_percentage: 'درصد کارمزد معامله', fee_service_charge_percentage: 'درصد کارمزد معامله',
fee_deduct_from_gold: 'اعمال مبلغ دقیق در معامله مشتری', fee_deduct_from_gold: 'اعمال مبلغ دقیق در معامله مشتری',
require_zero_main_gold_before_other_buy: 'الزام صفر بودن کیف اصلی قبل از خرید محصول دیگر',
fee_free: 'معافیت کارمزد سفارش اول', fee_free: 'معافیت کارمزد سفارش اول',
min_withdraw: 'حداقل مبلغ برداشت', min_withdraw: 'حداقل مبلغ برداشت',
min_deposit: 'حداقل مبلغ واریز کیف پول', min_deposit: 'حداقل مبلغ واریز کیف پول',
@@ -860,6 +864,7 @@ export const SETTING_LOCKED_KEYS = new Set([
'sms_panel', 'sms_panel',
'logo_login', 'logo_register', 'title_pwa', 'title_fav', 'logo_login', 'logo_register', 'title_pwa', 'title_fav',
'main_product', 'main_wallet_products', 'metal_type', 'main_metal', 'main_product', 'main_wallet_products', 'metal_type', 'main_metal',
'require_zero_main_gold_before_other_buy',
'montify_name', 'montify_name',
'montzar_base_url', 'montzar_closed', 'montzar_base_url', 'montzar_closed',
'montzar_diff', 'montzar_last_product_log', 'montzar_live_price', 'montzar_mazaneh', 'montzar_diff', 'montzar_last_product_log', 'montzar_live_price', 'montzar_mazaneh',
+28 -8
View File
@@ -293,16 +293,24 @@ async function setProductIndexImage(record: Record<string, any>, productId?: num
} }
async function edit(record: Product) { async function edit(record: Product) {
await loadCategorySelectItems() submitError.value = ''
addUserVisible.value = true
editId = record.id editId = record.id
let productData = cloneDeep(record) as Record<string, any> addUserVisible.value = true
loading.value = true
try { try {
const detail = unwrapApiRecord(await getCustomerById(record.id)) let productData = cloneDeep(record) as Record<string, any>
if (detail) productData = { ...productData, ...detail }
} catch { const [, detailResult] = await Promise.all([
/* use table row data */ loadCategorySelectItems(),
getCustomerById(record.id)
.then((res) => unwrapApiRecord(res))
.catch(() => null),
refreshLivePrices(),
])
if (detailResult) {
productData = { ...productData, ...detailResult }
} }
methodOption.value = productData.app_price methodOption.value = productData.app_price
@@ -354,12 +362,19 @@ async function edit(record: Product) {
}) })
await setProductIndexImage(productData, record.id) await setProductIndexImage(productData, record.id)
} catch (err) {
console.error(err)
message.error('خطا در دریافت اطلاعات محصول')
} finally {
loading.value = false
}
} }
function closeProductModal() { function closeProductModal() {
addUserVisible.value = false addUserVisible.value = false
editId = undefined editId = undefined
submitError.value = '' submitError.value = ''
loading.value = false
} }
async function add() { async function add() {
@@ -367,7 +382,12 @@ async function add() {
submitError.value = '' submitError.value = ''
addUserVisible.value = true addUserVisible.value = true
resetData() resetData()
await loadCategorySelectItems() loading.value = true
try {
await Promise.all([loadCategorySelectItems(), refreshLivePrices()])
} finally {
loading.value = false
}
} }
function resetData() { function resetData() {