Require review before Tahesab wallet sync and harden accounting admin UX.
Single-customer and row sync preview balances with documents before overwrite/keep; bulk sync keeps confirm. Also ship Zarin wallet sync actions, field help, and get_user cache improvements. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+149
-25
@@ -14,6 +14,9 @@
|
||||
const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL';
|
||||
const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL';
|
||||
const CONFIG_CACHE_KEY = 'APP_CONFIG_CACHE';
|
||||
const CONFIG_IDB_NAME = 'abshode_admin_cfg';
|
||||
const CONFIG_IDB_STORE = 'kv';
|
||||
const CONFIG_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
const VERSION_STORAGE_KEY = 'version';
|
||||
const VERSION_UPDATE_LOCK_KEY = 'version_update_lock';
|
||||
const VERSION_LAST_RELOAD_KEY = 'version_last_reload';
|
||||
@@ -130,13 +133,13 @@
|
||||
return readUserVersion(response.user);
|
||||
}
|
||||
|
||||
function readCachedAppConfig() {
|
||||
function readCachedAppConfigPayload() {
|
||||
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;
|
||||
return payload;
|
||||
} catch (error) {
|
||||
console.warn('Failed to read cached get_user:', error);
|
||||
localStorage.removeItem(CONFIG_CACHE_KEY);
|
||||
@@ -144,18 +147,125 @@
|
||||
}
|
||||
}
|
||||
|
||||
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 readCachedAppConfig() {
|
||||
const payload = readCachedAppConfigPayload();
|
||||
return payload && payload.config ? payload.config : null;
|
||||
}
|
||||
|
||||
function isFreshGetUserCache(payload, remoteVersion, domain) {
|
||||
if (!payload || !payload.config) return false;
|
||||
if (isServerUnreachable(payload.config)) return false;
|
||||
const cachedDomain = String(payload.domain || '').trim();
|
||||
if (cachedDomain && cachedDomain !== domain) return false;
|
||||
const age = Date.now() - Number(payload.cached_at || 0);
|
||||
if (!payload.cached_at || age < 0 || age > CONFIG_CACHE_MAX_AGE_MS) return false;
|
||||
const cachedVersion = normalizeVersion(payload.version);
|
||||
if (remoteVersion) {
|
||||
return cachedVersion === remoteVersion;
|
||||
}
|
||||
return Boolean(cachedVersion);
|
||||
}
|
||||
|
||||
function openConfigIdb() {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (!window.indexedDB) {
|
||||
reject(new Error('indexedDB unavailable'));
|
||||
return;
|
||||
}
|
||||
const req = window.indexedDB.open(CONFIG_IDB_NAME, 1);
|
||||
req.onupgradeneeded = function () {
|
||||
if (!req.result.objectStoreNames.contains(CONFIG_IDB_STORE)) {
|
||||
req.result.createObjectStore(CONFIG_IDB_STORE);
|
||||
}
|
||||
};
|
||||
req.onsuccess = function () {
|
||||
resolve(req.result);
|
||||
};
|
||||
req.onerror = function () {
|
||||
reject(req.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function writeCachedAppConfigToIdb(payload) {
|
||||
openConfigIdb()
|
||||
.then(function (db) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const tx = db.transaction(CONFIG_IDB_STORE, 'readwrite');
|
||||
tx.oncomplete = function () {
|
||||
resolve();
|
||||
};
|
||||
tx.onerror = function () {
|
||||
reject(tx.error);
|
||||
};
|
||||
tx.objectStore(CONFIG_IDB_STORE).put(payload, CONFIG_CACHE_KEY);
|
||||
});
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.warn('Failed to write get_user IndexedDB cache:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function readCachedAppConfigFromIdb() {
|
||||
return openConfigIdb()
|
||||
.then(function (db) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const tx = db.transaction(CONFIG_IDB_STORE, 'readonly');
|
||||
const req = tx.objectStore(CONFIG_IDB_STORE).get(CONFIG_CACHE_KEY);
|
||||
req.onsuccess = function () {
|
||||
const value = req.result;
|
||||
if (!value || typeof value !== 'object' || !value.config) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
resolve(value);
|
||||
};
|
||||
req.onerror = function () {
|
||||
reject(req.error);
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function clearCachedAppConfigIdb() {
|
||||
openConfigIdb()
|
||||
.then(function (db) {
|
||||
const tx = db.transaction(CONFIG_IDB_STORE, 'readwrite');
|
||||
tx.objectStore(CONFIG_IDB_STORE).delete(CONFIG_CACHE_KEY);
|
||||
})
|
||||
.catch(function () {
|
||||
/* ignore */
|
||||
});
|
||||
}
|
||||
|
||||
function writeCachedAppConfig(config, version) {
|
||||
const payload = {
|
||||
cached_at: Date.now(),
|
||||
version: normalizeVersion(version || getStoredAppVersion()),
|
||||
domain: resolveTenantDomain(),
|
||||
config: config
|
||||
};
|
||||
try {
|
||||
localStorage.setItem(CONFIG_CACHE_KEY, JSON.stringify(payload));
|
||||
clearCachedAppConfigIdb();
|
||||
} catch (error) {
|
||||
console.warn('get_user too large for localStorage; using IndexedDB', error);
|
||||
try {
|
||||
localStorage.removeItem(CONFIG_CACHE_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
writeCachedAppConfigToIdb(payload);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveCachedPayload() {
|
||||
const fromLs = readCachedAppConfigPayload();
|
||||
if (fromLs) return fromLs;
|
||||
return await readCachedAppConfigFromIdb();
|
||||
}
|
||||
|
||||
function getStoredAppVersion() {
|
||||
@@ -238,6 +348,7 @@
|
||||
localStorage.removeItem(CONFIG_CACHE_KEY);
|
||||
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY);
|
||||
localStorage.removeItem(VERSION_SUCCESS_URL_KEY);
|
||||
clearCachedAppConfigIdb();
|
||||
restoreAuthToken(token);
|
||||
}
|
||||
|
||||
@@ -311,7 +422,7 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchConfig(rounds) {
|
||||
async function fetchConfig(rounds, version) {
|
||||
rounds = rounds || CONFIG_RETRY_ROUNDS;
|
||||
const urls = sortUrls(getConfigUrls(), CONFIG_SUCCESS_URL_KEY);
|
||||
|
||||
@@ -328,7 +439,7 @@
|
||||
}
|
||||
|
||||
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url);
|
||||
writeCachedAppConfig(config);
|
||||
writeCachedAppConfig(config, version);
|
||||
return { config: config, url: url };
|
||||
} catch (error) {
|
||||
clearStaleSuccessUrl(CONFIG_SUCCESS_URL_KEY, url);
|
||||
@@ -413,15 +524,15 @@
|
||||
startVersionUpdateLock(serverVersion);
|
||||
|
||||
try {
|
||||
const { config } = await fetchConfig();
|
||||
const { config } = await fetchConfig(undefined, serverVersion);
|
||||
if (isServerUnreachable(config)) {
|
||||
writeCachedAppConfig(config);
|
||||
writeCachedAppConfig(config, serverVersion);
|
||||
showServerUnavailable();
|
||||
cancelVersionUpdateLock();
|
||||
return true;
|
||||
}
|
||||
clearBootstrapCachesKeepToken();
|
||||
writeCachedAppConfig(config);
|
||||
writeCachedAppConfig(config, serverVersion);
|
||||
applyConfig(config);
|
||||
finishVersionUpdateBeforeReload(serverVersion);
|
||||
console.log('App version changed — hard refresh', serverVersion);
|
||||
@@ -441,8 +552,8 @@
|
||||
let needsFreshForVersion = false;
|
||||
|
||||
try {
|
||||
// کش قدیمی را فقط اگر خودش قفل باشد اعمال/قطع کن؛ وگرنه تا get_user تازه صبر کن
|
||||
const cachedConfig = readCachedAppConfig();
|
||||
const cachedPayload = await resolveCachedPayload();
|
||||
const cachedConfig = cachedPayload && cachedPayload.config ? cachedPayload.config : null;
|
||||
if (cachedConfig && isServerUnreachable(cachedConfig)) {
|
||||
showServerUnavailable();
|
||||
return;
|
||||
@@ -461,9 +572,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
const { config, url } = await fetchConfig();
|
||||
const domain = resolveTenantDomain();
|
||||
if (
|
||||
!needsFreshForVersion &&
|
||||
isFreshGetUserCache(cachedPayload, remoteVersion, domain)
|
||||
) {
|
||||
applyConfig(cachedPayload.config);
|
||||
if (remoteVersion) {
|
||||
writeStoredAppVersion(remoteVersion);
|
||||
}
|
||||
console.log('Config loaded from cache; get_user skipped');
|
||||
finishConfigLoad(cachedPayload.config);
|
||||
return;
|
||||
}
|
||||
|
||||
const { config, url } = await fetchConfig(undefined, remoteVersion);
|
||||
applyConfig(config);
|
||||
writeCachedAppConfig(config);
|
||||
writeCachedAppConfig(config, remoteVersion);
|
||||
if (remoteVersion) {
|
||||
writeStoredAppVersion(remoteVersion);
|
||||
}
|
||||
@@ -477,13 +602,13 @@
|
||||
console.warn('Config loader: Could not fetch config from any endpoint');
|
||||
console.warn('Error details:', error && error.message ? error.message : error);
|
||||
|
||||
// اگر نسخه عوض شده (قفل/آنلاک)، هرگز با کش قدیمی reachable ادمین را بالا نیاور
|
||||
if (versionChanged || needsFreshForVersion) {
|
||||
showServerUnavailable();
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedConfig = readCachedAppConfig();
|
||||
const cachedPayload = await resolveCachedPayload();
|
||||
const cachedConfig = cachedPayload && cachedPayload.config ? cachedPayload.config : null;
|
||||
if (cachedConfig) {
|
||||
try {
|
||||
applyConfig(cachedConfig);
|
||||
@@ -519,7 +644,6 @@
|
||||
showServerUnavailable();
|
||||
return;
|
||||
}
|
||||
// بدون تأیید تازه از get_user، ادمین را با کش بالا نیاور (دور زدن قفل)
|
||||
showServerUnavailable();
|
||||
}
|
||||
}, CONFIG_TIMEOUT);
|
||||
|
||||
Reference in New Issue
Block a user