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:
2026-08-30 21:18:26 +03:30
co-authored by Cursor
parent 04cc03fe25
commit 1c4b2d4435
60 changed files with 2464 additions and 552 deletions
+149 -25
View File
@@ -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);