feat: Implementar limpieza automática de menús antiguos en el comedor y eliminar la tienda de apps
This commit is contained in:
@@ -8,7 +8,7 @@ function tableScroll(query) {
|
|||||||
//var secretTokenEl = document.getElementById("secretToken");
|
//var secretTokenEl = document.getElementById("secretToken");
|
||||||
var container = document.getElementById('container');
|
var container = document.getElementById('container');
|
||||||
|
|
||||||
function open_page(params, retryAfterExternalLoad = false) {
|
function open_page(params) {
|
||||||
// Clear stored event listeners and timers
|
// Clear stored event listeners and timers
|
||||||
EventListeners.GunJS = [];
|
EventListeners.GunJS = [];
|
||||||
EventListeners.Timeout.forEach((ev) => clearTimeout(ev));
|
EventListeners.Timeout.forEach((ev) => clearTimeout(ev));
|
||||||
@@ -32,21 +32,10 @@ function open_page(params, retryAfterExternalLoad = false) {
|
|||||||
var path = params.split(',');
|
var path = params.split(',');
|
||||||
var app = path[0];
|
var app = path[0];
|
||||||
if (!PAGES[app]) {
|
if (!PAGES[app]) {
|
||||||
if (!retryAfterExternalLoad && typeof TS_loadExternalAppsFromDB === 'function') {
|
|
||||||
TS_loadExternalAppsFromDB().then(() => {
|
|
||||||
open_page(params, true);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toastr.error('La app solicitada no existe.');
|
toastr.error('La app solicitada no existe.');
|
||||||
setUrlHash('index');
|
setUrlHash('index');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof TS_isAppInstalled === 'function' && !TS_isAppInstalled(app)) {
|
|
||||||
toastr.error('Esta app no está instalada. Instálala desde la Tienda de apps.');
|
|
||||||
setUrlHash('tienda_apps');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (path[1] == undefined) {
|
if (path[1] == undefined) {
|
||||||
PAGES[app].index();
|
PAGES[app].index();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1726,616 +1726,6 @@ var PERMS = {
|
|||||||
ADMIN: 'Administrador',
|
ADMIN: 'Administrador',
|
||||||
};
|
};
|
||||||
|
|
||||||
var TS_INSTALLED_APPS_CACHE = null;
|
|
||||||
var TS_INSTALLED_APPS_CACHE_KEY = '';
|
|
||||||
var TS_INSTALLED_APPS_LOADING = false;
|
|
||||||
var TS_ESSENTIAL_APPS = new Set(['index', 'personas', 'dataman']);
|
|
||||||
var TS_LOCKED_APPS = new Set(['index', 'personas', 'dataman']);
|
|
||||||
var TS_APPS_SYNC_LISTENER_ID = null;
|
|
||||||
var TS_APPS_SYNC_REFRESH_PENDING = false;
|
|
||||||
|
|
||||||
function TS_getAppInstallStorageKey() {
|
|
||||||
var dbName = 'telesec';
|
|
||||||
try {
|
|
||||||
dbName = typeof getDBName === 'function' ? getDBName() : 'telesec';
|
|
||||||
} catch (e) {
|
|
||||||
dbName = 'telesec';
|
|
||||||
}
|
|
||||||
var personaId = SUB_LOGGED_IN_ID || 'anon';
|
|
||||||
return 'TELESEC_INSTALLED_APPS::' + dbName + '::' + personaId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_getAppInstallDocId() {
|
|
||||||
var dbName = 'telesec';
|
|
||||||
try {
|
|
||||||
dbName = typeof getDBName === 'function' ? getDBName() : 'telesec';
|
|
||||||
} catch (e) {
|
|
||||||
dbName = 'telesec';
|
|
||||||
}
|
|
||||||
var personaId = SUB_LOGGED_IN_ID || 'anon';
|
|
||||||
return 'installed_apps::' + dbName + '::' + personaId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_buildDefaultInstalledSet() {
|
|
||||||
var installedSet = new Set();
|
|
||||||
TS_ESSENTIAL_APPS.forEach((key) => {
|
|
||||||
if (!PAGES[key]) return;
|
|
||||||
if (TS_isSystemApp(key)) return;
|
|
||||||
if (PAGES[key].ExternalApp === true) return;
|
|
||||||
if (PAGES[key].Esconder === true) return;
|
|
||||||
installedSet.add(key);
|
|
||||||
});
|
|
||||||
TS_LOCKED_APPS.forEach((key) => {
|
|
||||||
if (PAGES[key] && PAGES[key].Esconder !== true) {
|
|
||||||
installedSet.add(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return installedSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_parseInstalledAppsPayload(payload) {
|
|
||||||
if (!payload || !Array.isArray(payload.apps)) return null;
|
|
||||||
var set = new Set();
|
|
||||||
payload.apps.forEach((appKey) => {
|
|
||||||
if (typeof appKey === 'string' && appKey.trim() !== '') {
|
|
||||||
set.add(appKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return set;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_readInstalledAppsFallback() {
|
|
||||||
try {
|
|
||||||
var raw = localStorage.getItem(TS_getAppInstallStorageKey());
|
|
||||||
if (!raw) return null;
|
|
||||||
var parsed = JSON.parse(raw);
|
|
||||||
return TS_parseInstalledAppsPayload(parsed);
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_writeInstalledAppsFallback(appSet) {
|
|
||||||
var apps = [];
|
|
||||||
if (appSet && typeof appSet.forEach === 'function') {
|
|
||||||
appSet.forEach((appKey) => {
|
|
||||||
if (typeof appKey === 'string' && appKey.trim() !== '') {
|
|
||||||
apps.push(appKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
localStorage.setItem(
|
|
||||||
TS_getAppInstallStorageKey(),
|
|
||||||
JSON.stringify({ version: 1, apps: apps.sort(), updatedAt: CurrentISOTime() })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_setInstalledAppsCache(appSet) {
|
|
||||||
TS_LOCKED_APPS.forEach((key) => {
|
|
||||||
if (PAGES[key] && PAGES[key].Esconder !== true) {
|
|
||||||
appSet.add(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
TS_INSTALLED_APPS_CACHE = appSet;
|
|
||||||
TS_INSTALLED_APPS_CACHE_KEY = TS_getAppInstallDocId();
|
|
||||||
TS_writeInstalledAppsFallback(appSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_persistInstalledAppsToDB(appSet) {
|
|
||||||
if (!window.DB || typeof DB.put !== 'function') {
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
var apps = [];
|
|
||||||
appSet.forEach((appKey) => {
|
|
||||||
if (typeof appKey === 'string' && appKey.trim() !== '') {
|
|
||||||
apps.push(appKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return DB.put('config', TS_getAppInstallDocId(), {
|
|
||||||
version: 1,
|
|
||||||
apps: apps.sort(),
|
|
||||||
updatedAt: CurrentISOTime(),
|
|
||||||
})
|
|
||||||
.then(() => true)
|
|
||||||
.catch((e) => {
|
|
||||||
console.warn('Error saving installed apps in DB', e);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_loadInstalledAppsFromDB(forceReload = false) {
|
|
||||||
var expectedKey = TS_getAppInstallDocId();
|
|
||||||
if (!forceReload && TS_INSTALLED_APPS_CACHE && TS_INSTALLED_APPS_CACHE_KEY === expectedKey) {
|
|
||||||
return Promise.resolve(TS_INSTALLED_APPS_CACHE);
|
|
||||||
}
|
|
||||||
if (TS_INSTALLED_APPS_LOADING) {
|
|
||||||
return Promise.resolve(TS_INSTALLED_APPS_CACHE);
|
|
||||||
}
|
|
||||||
TS_INSTALLED_APPS_LOADING = true;
|
|
||||||
|
|
||||||
var fallbackSet = TS_readInstalledAppsFallback() || TS_buildDefaultInstalledSet();
|
|
||||||
TS_setInstalledAppsCache(fallbackSet);
|
|
||||||
|
|
||||||
if (!window.DB || typeof DB.get !== 'function') {
|
|
||||||
TS_INSTALLED_APPS_LOADING = false;
|
|
||||||
return Promise.resolve(TS_INSTALLED_APPS_CACHE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DB.get('config', expectedKey)
|
|
||||||
.then((raw) => {
|
|
||||||
if (raw == null) {
|
|
||||||
TS_INSTALLED_APPS_LOADING = false;
|
|
||||||
return TS_persistInstalledAppsToDB(TS_INSTALLED_APPS_CACHE).then(() => TS_INSTALLED_APPS_CACHE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (typeof raw === 'string') {
|
|
||||||
TS_decrypt(
|
|
||||||
raw,
|
|
||||||
SECRET,
|
|
||||||
(decrypted) => {
|
|
||||||
var parsed = TS_parseInstalledAppsPayload(decrypted);
|
|
||||||
if (parsed) {
|
|
||||||
TS_setInstalledAppsCache(parsed);
|
|
||||||
}
|
|
||||||
TS_INSTALLED_APPS_LOADING = false;
|
|
||||||
resolve(TS_INSTALLED_APPS_CACHE);
|
|
||||||
},
|
|
||||||
'config',
|
|
||||||
expectedKey
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
var parsed = TS_parseInstalledAppsPayload(raw);
|
|
||||||
if (parsed) {
|
|
||||||
TS_setInstalledAppsCache(parsed);
|
|
||||||
}
|
|
||||||
TS_INSTALLED_APPS_LOADING = false;
|
|
||||||
resolve(TS_INSTALLED_APPS_CACHE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.warn('Error loading installed apps from DB', e);
|
|
||||||
TS_INSTALLED_APPS_LOADING = false;
|
|
||||||
return TS_INSTALLED_APPS_CACHE;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_getInstalledAppsSet() {
|
|
||||||
var expectedKey = TS_getAppInstallDocId();
|
|
||||||
if (!TS_INSTALLED_APPS_CACHE || TS_INSTALLED_APPS_CACHE_KEY !== expectedKey) {
|
|
||||||
TS_loadInstalledAppsFromDB().then(() => {
|
|
||||||
SetPages();
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return TS_INSTALLED_APPS_CACHE;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_setInstalledAppsSet(appSet) {
|
|
||||||
TS_setInstalledAppsCache(appSet);
|
|
||||||
TS_persistInstalledAppsToDB(appSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_isSystemApp(appKey) {
|
|
||||||
if (appKey === 'login') return true;
|
|
||||||
if (!PAGES[appKey]) return false;
|
|
||||||
return PAGES[appKey].SystemApp === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_isLockedApp(appKey) {
|
|
||||||
return TS_LOCKED_APPS.has(appKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_isMandatoryApp(appKey) {
|
|
||||||
return TS_isSystemApp(appKey) || TS_isLockedApp(appKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_isAppInstalled(appKey) {
|
|
||||||
if (TS_isMandatoryApp(appKey)) return true;
|
|
||||||
var installedSet = TS_getInstalledAppsSet();
|
|
||||||
if (installedSet == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return installedSet.has(appKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_installApp(appKey) {
|
|
||||||
if (!PAGES[appKey] || TS_isSystemApp(appKey)) return;
|
|
||||||
var installedSet = TS_getInstalledAppsSet();
|
|
||||||
if (installedSet == null) {
|
|
||||||
installedSet = TS_buildDefaultInstalledSet();
|
|
||||||
}
|
|
||||||
installedSet.add(appKey);
|
|
||||||
if (appKey === 'supercafe' && PAGES.pagos) {
|
|
||||||
installedSet.add('pagos');
|
|
||||||
}
|
|
||||||
TS_setInstalledAppsSet(installedSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_uninstallApp(appKey) {
|
|
||||||
if (!PAGES[appKey] || TS_isMandatoryApp(appKey)) return;
|
|
||||||
var installedSet = TS_getInstalledAppsSet();
|
|
||||||
if (installedSet == null) {
|
|
||||||
installedSet = TS_buildDefaultInstalledSet();
|
|
||||||
}
|
|
||||||
installedSet.delete(appKey);
|
|
||||||
TS_setInstalledAppsSet(installedSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_resetInstalledApps() {
|
|
||||||
var defaults = TS_buildDefaultInstalledSet();
|
|
||||||
TS_setInstalledAppsSet(defaults);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_getAppCatalog() {
|
|
||||||
return Object.keys(PAGES)
|
|
||||||
.filter((key) => PAGES[key].Esconder !== true)
|
|
||||||
.map((key) => {
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
title: PAGES[key].Title || key,
|
|
||||||
icon: PAGES[key].icon || 'static/appico/application_enterprise.png',
|
|
||||||
installed: TS_isAppInstalled(key),
|
|
||||||
system: TS_isSystemApp(key),
|
|
||||||
canAccess: !PAGES[key].AccessControl || checkRole(key),
|
|
||||||
requiresRole: PAGES[key].AccessControl === true,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a, b) => a.title.localeCompare(b.title, 'es'));
|
|
||||||
}
|
|
||||||
|
|
||||||
var TS_EXTERNAL_APPS_CACHE = [];
|
|
||||||
var TS_EXTERNAL_APPS_CACHE_KEY = '';
|
|
||||||
var TS_EXTERNAL_APPS_LOADING = false;
|
|
||||||
var TS_EXTERNAL_APPS_READY = false;
|
|
||||||
|
|
||||||
function TS_getExternalAppsStorageKey() {
|
|
||||||
var dbName = 'telesec';
|
|
||||||
try {
|
|
||||||
dbName = typeof getDBName === 'function' ? getDBName() : 'telesec';
|
|
||||||
} catch (e) {
|
|
||||||
dbName = 'telesec';
|
|
||||||
}
|
|
||||||
var personaId = SUB_LOGGED_IN_ID || 'anon';
|
|
||||||
return 'TELESEC_EXTERNAL_APPS::' + dbName + '::' + personaId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_getExternalAppsDocId() {
|
|
||||||
var dbName = 'telesec';
|
|
||||||
try {
|
|
||||||
dbName = typeof getDBName === 'function' ? getDBName() : 'telesec';
|
|
||||||
} catch (e) {
|
|
||||||
dbName = 'telesec';
|
|
||||||
}
|
|
||||||
var personaId = SUB_LOGGED_IN_ID || 'anon';
|
|
||||||
return 'external_apps::' + dbName + '::' + personaId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_parseExternalAppsPayload(payload) {
|
|
||||||
if (!payload || !Array.isArray(payload.apps)) return [];
|
|
||||||
return payload.apps.filter((entry) => {
|
|
||||||
return (
|
|
||||||
entry &&
|
|
||||||
typeof entry.appKey === 'string' &&
|
|
||||||
entry.appKey.trim() !== '' &&
|
|
||||||
typeof entry.code === 'string' &&
|
|
||||||
entry.code.trim() !== ''
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_readExternalAppsFallback() {
|
|
||||||
try {
|
|
||||||
var raw = localStorage.getItem(TS_getExternalAppsStorageKey());
|
|
||||||
if (!raw) return [];
|
|
||||||
return TS_parseExternalAppsPayload(JSON.parse(raw));
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_writeExternalAppsFallback(appsList) {
|
|
||||||
localStorage.setItem(
|
|
||||||
TS_getExternalAppsStorageKey(),
|
|
||||||
JSON.stringify({
|
|
||||||
version: 1,
|
|
||||||
apps: appsList,
|
|
||||||
updatedAt: CurrentISOTime(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_setExternalAppsCache(appsList) {
|
|
||||||
TS_EXTERNAL_APPS_CACHE = Array.isArray(appsList) ? appsList : [];
|
|
||||||
TS_EXTERNAL_APPS_CACHE_KEY = TS_getExternalAppsDocId();
|
|
||||||
TS_EXTERNAL_APPS_READY = true;
|
|
||||||
TS_writeExternalAppsFallback(TS_EXTERNAL_APPS_CACHE);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_saveExternalAppsToDB(appsList) {
|
|
||||||
if (!window.DB || typeof DB.put !== 'function') {
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
return DB.put('config', TS_getExternalAppsDocId(), {
|
|
||||||
version: 1,
|
|
||||||
apps: appsList,
|
|
||||||
updatedAt: CurrentISOTime(),
|
|
||||||
})
|
|
||||||
.then(() => true)
|
|
||||||
.catch((e) => {
|
|
||||||
console.warn('Error saving external apps in DB', e);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_evalExternalAppCode(code, expectedAppKey = '') {
|
|
||||||
if (typeof code !== 'string' || code.trim() === '') {
|
|
||||||
throw new Error('Código vacío en app externa.');
|
|
||||||
}
|
|
||||||
|
|
||||||
var beforeKeys = Object.keys(PAGES);
|
|
||||||
var beforeSet = new Set(beforeKeys);
|
|
||||||
try {
|
|
||||||
new Function(code)();
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('Error ejecutando app externa: ' + (e && e.message ? e.message : e));
|
|
||||||
}
|
|
||||||
|
|
||||||
var afterKeys = Object.keys(PAGES);
|
|
||||||
var newKeys = afterKeys.filter((key) => !beforeSet.has(key));
|
|
||||||
var appKey = expectedAppKey || '';
|
|
||||||
|
|
||||||
if (!appKey) {
|
|
||||||
if (newKeys.length === 0) {
|
|
||||||
throw new Error('La app externa no registró ninguna entrada en PAGES.');
|
|
||||||
}
|
|
||||||
appKey = newKeys[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PAGES[appKey]) {
|
|
||||||
throw new Error('No se pudo detectar la app externa cargada.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (beforeSet.has(appKey) && PAGES[appKey].ExternalApp !== true) {
|
|
||||||
throw new Error('La app externa intenta sobrescribir una app interna: ' + appKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
PAGES[appKey].ExternalApp = true;
|
|
||||||
PAGES[appKey].SystemApp = false;
|
|
||||||
|
|
||||||
return {
|
|
||||||
appKey: appKey,
|
|
||||||
title: PAGES[appKey].Title || appKey,
|
|
||||||
icon: PAGES[appKey].icon || 'static/appico/application_enterprise.png',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_applyExternalAppsRegistry(appsList) {
|
|
||||||
appsList.forEach((entry) => {
|
|
||||||
try {
|
|
||||||
if (entry.appKey && PAGES[entry.appKey] && PAGES[entry.appKey].ExternalApp === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
TS_evalExternalAppCode(entry.code, entry.appKey || '');
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Error loading external app', entry && entry.appKey, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_loadExternalAppsFromDB(forceReload = false) {
|
|
||||||
var expectedKey = TS_getExternalAppsDocId();
|
|
||||||
if (!forceReload && TS_EXTERNAL_APPS_READY && TS_EXTERNAL_APPS_CACHE_KEY === expectedKey) {
|
|
||||||
return Promise.resolve(TS_EXTERNAL_APPS_CACHE);
|
|
||||||
}
|
|
||||||
if (TS_EXTERNAL_APPS_LOADING) {
|
|
||||||
return Promise.resolve(TS_EXTERNAL_APPS_CACHE);
|
|
||||||
}
|
|
||||||
TS_EXTERNAL_APPS_LOADING = true;
|
|
||||||
|
|
||||||
var fallback = TS_readExternalAppsFallback();
|
|
||||||
TS_setExternalAppsCache(fallback);
|
|
||||||
TS_applyExternalAppsRegistry(TS_EXTERNAL_APPS_CACHE);
|
|
||||||
|
|
||||||
if (!window.DB || typeof DB.get !== 'function') {
|
|
||||||
TS_EXTERNAL_APPS_LOADING = false;
|
|
||||||
return Promise.resolve(TS_EXTERNAL_APPS_CACHE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DB.get('config', expectedKey)
|
|
||||||
.then((raw) => {
|
|
||||||
if (raw == null) {
|
|
||||||
TS_EXTERNAL_APPS_LOADING = false;
|
|
||||||
return TS_saveExternalAppsToDB(TS_EXTERNAL_APPS_CACHE).then(() => TS_EXTERNAL_APPS_CACHE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (typeof raw === 'string') {
|
|
||||||
TS_decrypt(
|
|
||||||
raw,
|
|
||||||
SECRET,
|
|
||||||
(decrypted) => {
|
|
||||||
var parsed = TS_parseExternalAppsPayload(decrypted);
|
|
||||||
TS_setExternalAppsCache(parsed);
|
|
||||||
TS_applyExternalAppsRegistry(TS_EXTERNAL_APPS_CACHE);
|
|
||||||
TS_EXTERNAL_APPS_LOADING = false;
|
|
||||||
resolve(TS_EXTERNAL_APPS_CACHE);
|
|
||||||
},
|
|
||||||
'config',
|
|
||||||
expectedKey
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
var parsed = TS_parseExternalAppsPayload(raw);
|
|
||||||
TS_setExternalAppsCache(parsed);
|
|
||||||
TS_applyExternalAppsRegistry(TS_EXTERNAL_APPS_CACHE);
|
|
||||||
TS_EXTERNAL_APPS_LOADING = false;
|
|
||||||
resolve(TS_EXTERNAL_APPS_CACHE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.warn('Error loading external apps from DB', e);
|
|
||||||
TS_EXTERNAL_APPS_LOADING = false;
|
|
||||||
return TS_EXTERNAL_APPS_CACHE;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_getExternalAppsCatalog() {
|
|
||||||
var expectedKey = TS_getExternalAppsDocId();
|
|
||||||
if (!TS_EXTERNAL_APPS_READY || TS_EXTERNAL_APPS_CACHE_KEY !== expectedKey) {
|
|
||||||
TS_loadExternalAppsFromDB().then(() => {
|
|
||||||
SetPages();
|
|
||||||
});
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return TS_EXTERNAL_APPS_CACHE.map((entry) => {
|
|
||||||
return {
|
|
||||||
appKey: entry.appKey,
|
|
||||||
title: entry.title || entry.appKey,
|
|
||||||
sourceType: entry.sourceType || 'file',
|
|
||||||
source: entry.source || '',
|
|
||||||
installedAt: entry.installedAt || '',
|
|
||||||
installed: TS_isAppInstalled(entry.appKey),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_installExternalAppFromCode(code, sourceType = 'file', sourceRef = '') {
|
|
||||||
return TS_loadExternalAppsFromDB().then(() => {
|
|
||||||
var info = TS_evalExternalAppCode(code, '');
|
|
||||||
var next = TS_EXTERNAL_APPS_CACHE.filter((entry) => entry.appKey !== info.appKey);
|
|
||||||
var item = {
|
|
||||||
id: safeuuid('extapp-'),
|
|
||||||
appKey: info.appKey,
|
|
||||||
title: info.title,
|
|
||||||
icon: info.icon,
|
|
||||||
sourceType: sourceType,
|
|
||||||
source: sourceRef,
|
|
||||||
code: code,
|
|
||||||
installedAt: CurrentISOTime(),
|
|
||||||
updatedAt: CurrentISOTime(),
|
|
||||||
};
|
|
||||||
next.push(item);
|
|
||||||
TS_setExternalAppsCache(next);
|
|
||||||
TS_saveExternalAppsToDB(next);
|
|
||||||
TS_installApp(info.appKey);
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_uninstallExternalApp(appKey) {
|
|
||||||
return TS_loadExternalAppsFromDB().then(() => {
|
|
||||||
var next = TS_EXTERNAL_APPS_CACHE.filter((entry) => entry.appKey !== appKey);
|
|
||||||
TS_setExternalAppsCache(next);
|
|
||||||
TS_saveExternalAppsToDB(next);
|
|
||||||
TS_uninstallApp(appKey);
|
|
||||||
if (PAGES[appKey] && PAGES[appKey].ExternalApp === true) {
|
|
||||||
delete PAGES[appKey];
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_resetAppsToDefault() {
|
|
||||||
return TS_loadExternalAppsFromDB().then(() => {
|
|
||||||
TS_EXTERNAL_APPS_CACHE.forEach((entry) => {
|
|
||||||
var appKey = entry.appKey;
|
|
||||||
if (PAGES[appKey] && PAGES[appKey].ExternalApp === true) {
|
|
||||||
delete PAGES[appKey];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
TS_setExternalAppsCache([]);
|
|
||||||
TS_saveExternalAppsToDB([]);
|
|
||||||
TS_resetInstalledApps();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_queueAppsUIRefresh() {
|
|
||||||
if (TS_APPS_SYNC_REFRESH_PENDING) return;
|
|
||||||
TS_APPS_SYNC_REFRESH_PENDING = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
TS_APPS_SYNC_REFRESH_PENDING = false;
|
|
||||||
try {
|
|
||||||
SetPages();
|
|
||||||
var currentPage = location.hash.replace('#', '').split('?')[0].split(',')[0];
|
|
||||||
if (currentPage === 'tienda_apps' && PAGES.tienda_apps && typeof PAGES.tienda_apps.index === 'function') {
|
|
||||||
PAGES.tienda_apps.index();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Apps UI refresh warning', e);
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_applyInstalledAppsFromConfigValue(raw, key) {
|
|
||||||
if (key !== TS_getAppInstallDocId()) return;
|
|
||||||
if (raw == null) {
|
|
||||||
TS_setInstalledAppsCache(TS_buildDefaultInstalledSet());
|
|
||||||
TS_queueAppsUIRefresh();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof raw === 'string') {
|
|
||||||
TS_decrypt(
|
|
||||||
raw,
|
|
||||||
SECRET,
|
|
||||||
(decrypted) => {
|
|
||||||
var parsed = TS_parseInstalledAppsPayload(decrypted);
|
|
||||||
if (parsed) {
|
|
||||||
TS_setInstalledAppsCache(parsed);
|
|
||||||
TS_queueAppsUIRefresh();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'config',
|
|
||||||
key
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var parsed = TS_parseInstalledAppsPayload(raw);
|
|
||||||
if (parsed) {
|
|
||||||
TS_setInstalledAppsCache(parsed);
|
|
||||||
TS_queueAppsUIRefresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_applyExternalAppsFromConfigValue(raw, key) {
|
|
||||||
if (key !== TS_getExternalAppsDocId()) return;
|
|
||||||
if (raw == null) {
|
|
||||||
TS_setExternalAppsCache([]);
|
|
||||||
TS_queueAppsUIRefresh();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof raw === 'string') {
|
|
||||||
TS_decrypt(
|
|
||||||
raw,
|
|
||||||
SECRET,
|
|
||||||
(decrypted) => {
|
|
||||||
var parsed = TS_parseExternalAppsPayload(decrypted);
|
|
||||||
TS_setExternalAppsCache(parsed);
|
|
||||||
TS_applyExternalAppsRegistry(TS_EXTERNAL_APPS_CACHE);
|
|
||||||
TS_queueAppsUIRefresh();
|
|
||||||
},
|
|
||||||
'config',
|
|
||||||
key
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var parsed = TS_parseExternalAppsPayload(raw);
|
|
||||||
TS_setExternalAppsCache(parsed);
|
|
||||||
TS_applyExternalAppsRegistry(TS_EXTERNAL_APPS_CACHE);
|
|
||||||
TS_queueAppsUIRefresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
function TS_initAppsRealtimeSync() {
|
|
||||||
if (!window.DB || typeof DB.map !== 'function') return;
|
|
||||||
if (TS_APPS_SYNC_LISTENER_ID) return;
|
|
||||||
TS_APPS_SYNC_LISTENER_ID = DB.map('config', (data, key) => {
|
|
||||||
TS_applyInstalledAppsFromConfigValue(data, key);
|
|
||||||
TS_applyExternalAppsFromConfigValue(data, key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkRole(role) {
|
function checkRole(role) {
|
||||||
var roles = SUB_LOGGED_IN_DETAILS.Roles || '';
|
var roles = SUB_LOGGED_IN_DETAILS.Roles || '';
|
||||||
var rolesArr = roles.split(',');
|
var rolesArr = roles.split(',');
|
||||||
@@ -2346,21 +1736,11 @@ function checkRole(role) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function SetPages() {
|
function SetPages() {
|
||||||
TS_initAppsRealtimeSync();
|
|
||||||
var expectedExternalKey = TS_getExternalAppsDocId();
|
|
||||||
if (!TS_EXTERNAL_APPS_READY || TS_EXTERNAL_APPS_CACHE_KEY !== expectedExternalKey) {
|
|
||||||
TS_loadExternalAppsFromDB().then(() => {
|
|
||||||
SetPages();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
document.getElementById('appendApps2').innerHTML = '';
|
document.getElementById('appendApps2').innerHTML = '';
|
||||||
Object.keys(PAGES).forEach((key) => {
|
Object.keys(PAGES).forEach((key) => {
|
||||||
if (PAGES[key].Esconder == true) {
|
if (PAGES[key].Esconder == true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!TS_isAppInstalled(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (PAGES[key].AccessControl == true) {
|
if (PAGES[key].AccessControl == true) {
|
||||||
var roles = SUB_LOGGED_IN_DETAILS.Roles || '';
|
var roles = SUB_LOGGED_IN_DETAILS.Roles || '';
|
||||||
var rolesArr = roles.split(',');
|
var rolesArr = roles.split(',');
|
||||||
|
|||||||
@@ -95,7 +95,6 @@
|
|||||||
<!-- <script src="page/chat.js"></script> -->
|
<!-- <script src="page/chat.js"></script> -->
|
||||||
<script src="page/buscar.js"></script>
|
<script src="page/buscar.js"></script>
|
||||||
<script src="page/pagos.js"></script>
|
<script src="page/pagos.js"></script>
|
||||||
<script src="page/tienda_apps.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -5,6 +5,66 @@ PAGES.comedor = {
|
|||||||
icon: 'static/appico/apple.png',
|
icon: 'static/appico/apple.png',
|
||||||
AccessControl: true,
|
AccessControl: true,
|
||||||
Title: 'Comedor',
|
Title: 'Comedor',
|
||||||
|
__cleanupOldMenus: async function () {
|
||||||
|
try {
|
||||||
|
var rows = await DB.list('comedor');
|
||||||
|
var now = new Date();
|
||||||
|
var todayUTC = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
||||||
|
var removed = 0;
|
||||||
|
|
||||||
|
function parseISODateToUTC(value) {
|
||||||
|
if (!value || typeof value !== 'string') return null;
|
||||||
|
var match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) return null;
|
||||||
|
var y = parseInt(match[1], 10);
|
||||||
|
var m = parseInt(match[2], 10) - 1;
|
||||||
|
var d = parseInt(match[3], 10);
|
||||||
|
return Date.UTC(y, m, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFechaFromRow(row) {
|
||||||
|
var data = row.data;
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
TS_decrypt(
|
||||||
|
data,
|
||||||
|
SECRET,
|
||||||
|
(decrypted) => {
|
||||||
|
if (decrypted && typeof decrypted === 'object') {
|
||||||
|
resolve(decrypted.Fecha || row.id.split(',')[0] || '');
|
||||||
|
} else {
|
||||||
|
resolve(row.id.split(',')[0] || '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'comedor',
|
||||||
|
row.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
return data.Fecha || row.id.split(',')[0] || '';
|
||||||
|
}
|
||||||
|
return row.id.split(',')[0] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
var fecha = await getFechaFromRow(row);
|
||||||
|
var rowUTC = parseISODateToUTC(fecha);
|
||||||
|
if (rowUTC == null) continue;
|
||||||
|
var ageDays = Math.floor((todayUTC - rowUTC) / 86400000);
|
||||||
|
if (ageDays >= 30) {
|
||||||
|
await DB.del('comedor', row.id);
|
||||||
|
removed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removed > 0) {
|
||||||
|
toastr.info('Limpieza automática: ' + removed + ' menús antiguos eliminados.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Comedor cleanup error', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
edit: function (mid) {
|
edit: function (mid) {
|
||||||
if (!checkRole('comedor:edit')) {
|
if (!checkRole('comedor:edit')) {
|
||||||
setUrlHash('comedor');
|
setUrlHash('comedor');
|
||||||
@@ -161,6 +221,7 @@ PAGES.comedor = {
|
|||||||
<button id="${btn_new}">Nueva entrada</button>
|
<button id="${btn_new}">Nueva entrada</button>
|
||||||
<div id="${cont}"></div>
|
<div id="${cont}"></div>
|
||||||
`;
|
`;
|
||||||
|
var renderList = () => {
|
||||||
TS_IndexElement(
|
TS_IndexElement(
|
||||||
'comedor',
|
'comedor',
|
||||||
[
|
[
|
||||||
@@ -207,6 +268,9 @@ PAGES.comedor = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PAGES.comedor.__cleanupOldMenus().finally(renderList);
|
||||||
|
|
||||||
if (!checkRole('comedor:edit')) {
|
if (!checkRole('comedor:edit')) {
|
||||||
document.getElementById(btn_new).style.display = 'none';
|
document.getElementById(btn_new).style.display = 'none';
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
PAGES.tienda_apps = {
|
|
||||||
Title: 'Tienda de apps',
|
|
||||||
icon: 'static/appico/application_enterprise.png',
|
|
||||||
SystemApp: true,
|
|
||||||
AccessControl: true,
|
|
||||||
AccessControlRole: "admin",
|
|
||||||
index: function () {
|
|
||||||
if (!checkRole('admin')) {
|
|
||||||
setUrlHash('index');
|
|
||||||
toastr.error('No tienes permiso para acceder a la tienda de apps.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var appsContainerId = safeuuid();
|
|
||||||
var externalContainerId = safeuuid();
|
|
||||||
var fieldFileId = safeuuid();
|
|
||||||
var btnFileInstallId = safeuuid();
|
|
||||||
var btnResetId = safeuuid();
|
|
||||||
|
|
||||||
container.innerHTML = html`
|
|
||||||
<h1>Tienda de apps</h1>
|
|
||||||
<p>Instala o desinstala módulos personalizados para tu cuenta actual.</p>
|
|
||||||
<button id="${btnResetId}" class="btn3">Reestablecer apps</button>
|
|
||||||
|
|
||||||
<h2>Apps oficiales</h2>
|
|
||||||
<div
|
|
||||||
id="${appsContainerId}"
|
|
||||||
style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px;align-items:stretch;"
|
|
||||||
></div>
|
|
||||||
<h2>Apps externas instaladas</h2>
|
|
||||||
<div
|
|
||||||
id="${externalContainerId}"
|
|
||||||
style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px;align-items:stretch;"
|
|
||||||
></div>
|
|
||||||
<fieldset>
|
|
||||||
<legend>Instalar app externa</legend>
|
|
||||||
<label>
|
|
||||||
Archivo .telejs
|
|
||||||
<input id="${fieldFileId}" type="file" accept=".telejs,.js,text/javascript" />
|
|
||||||
</label>
|
|
||||||
<button id="${btnFileInstallId}" class="btn5" type="button">Instalar archivo</button>
|
|
||||||
<br />
|
|
||||||
<small>⚠️ Solo instala apps de fuentes de confianza.</small>
|
|
||||||
</fieldset>
|
|
||||||
`;
|
|
||||||
|
|
||||||
var render = () => {
|
|
||||||
var catalog = TS_getAppCatalog().filter((app) => app.key !== 'index' && app.key !== 'tienda_apps');
|
|
||||||
if (catalog.length === 0) {
|
|
||||||
document.getElementById(appsContainerId).innerHTML = '<i>No hay apps disponibles en el catálogo.</i>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var htmlCards = catalog
|
|
||||||
.map((app) => {
|
|
||||||
var roleInfo = app.requiresRole
|
|
||||||
? app.canAccess
|
|
||||||
? '<small>Permiso: OK</small>'
|
|
||||||
: '<small style="color: #b22222;">Sin permiso de acceso</small>'
|
|
||||||
: '<small>Permiso: no requerido</small>';
|
|
||||||
|
|
||||||
var actionBtn = app.installed
|
|
||||||
? TS_isMandatoryApp(app.key)
|
|
||||||
? `<button class="btn3" type="button" disabled title="Esta app no se puede desinstalar">Esencial</button>`
|
|
||||||
: `<button class="btn3" data-action="uninstall" data-app="${app.key}">Desinstalar</button>`
|
|
||||||
: `<button class="btn5" data-action="install" data-app="${app.key}">Instalar</button>`;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<fieldset style="margin:0; height: 100%; box-sizing: border-box;">
|
|
||||||
<legend>${app.title}</legend>
|
|
||||||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
|
|
||||||
<img src="${app.icon}" alt="${app.title}" style="width:32px;height:32px;" />
|
|
||||||
<b>${app.title}</b>
|
|
||||||
<span>${app.installed ? '✅ Instalada' : '⬜ No instalada'}</span>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:8px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
|
||||||
${actionBtn}
|
|
||||||
${roleInfo}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
var target = document.getElementById(appsContainerId);
|
|
||||||
target.innerHTML = htmlCards;
|
|
||||||
|
|
||||||
target.querySelectorAll('button[data-action]').forEach((button) => {
|
|
||||||
button.onclick = () => {
|
|
||||||
var appKey = button.getAttribute('data-app');
|
|
||||||
var action = button.getAttribute('data-action');
|
|
||||||
if (action === 'install') {
|
|
||||||
TS_installApp(appKey);
|
|
||||||
toastr.success('App instalada: ' + appKey);
|
|
||||||
} else {
|
|
||||||
TS_uninstallApp(appKey);
|
|
||||||
toastr.info('App desinstalada: ' + appKey);
|
|
||||||
}
|
|
||||||
SetPages();
|
|
||||||
render();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
var external = TS_getExternalAppsCatalog();
|
|
||||||
var externalTarget = document.getElementById(externalContainerId);
|
|
||||||
if (!external.length) {
|
|
||||||
externalTarget.innerHTML = '<i>No hay apps externas instaladas.</i>';
|
|
||||||
} else {
|
|
||||||
externalTarget.innerHTML = external
|
|
||||||
.map((entry) => {
|
|
||||||
return `
|
|
||||||
<fieldset style="margin:0; height: 100%; box-sizing: border-box;">
|
|
||||||
<legend>${entry.title}</legend>
|
|
||||||
<div><b>Clave:</b> ${entry.appKey}</div>
|
|
||||||
<div><b>Origen:</b> ${entry.sourceType} ${entry.source ? '- ' + entry.source : ''}</div>
|
|
||||||
<div style="margin-top:8px;">
|
|
||||||
<button class="btn3" data-external-action="uninstall" data-external-app="${entry.appKey}">
|
|
||||||
Desinstalar externa
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
externalTarget.querySelectorAll('button[data-external-action]').forEach((button) => {
|
|
||||||
button.onclick = async () => {
|
|
||||||
var appKey = button.getAttribute('data-external-app');
|
|
||||||
await TS_uninstallExternalApp(appKey);
|
|
||||||
toastr.info('App externa desinstalada: ' + appKey);
|
|
||||||
SetPages();
|
|
||||||
render();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById(btnResetId).onclick = async () => {
|
|
||||||
await TS_resetAppsToDefault();
|
|
||||||
SetPages();
|
|
||||||
render();
|
|
||||||
toastr.success('Apps reestablecidas al estado por defecto.');
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById(btnFileInstallId).onclick = async () => {
|
|
||||||
var input = document.getElementById(fieldFileId);
|
|
||||||
if (!input.files || !input.files[0]) {
|
|
||||||
toastr.error('Selecciona un archivo .telejs');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var file = input.files[0];
|
|
||||||
try {
|
|
||||||
var code = await new Promise((resolve, reject) => {
|
|
||||||
var reader = new FileReader();
|
|
||||||
reader.onload = (ev) => resolve(ev.target.result || '');
|
|
||||||
reader.onerror = (err) => reject(err);
|
|
||||||
reader.readAsText(file);
|
|
||||||
});
|
|
||||||
var entry = await TS_installExternalAppFromCode(code, 'file', file.name || 'archivo.telejs');
|
|
||||||
toastr.success('App externa instalada: ' + entry.appKey);
|
|
||||||
SetPages();
|
|
||||||
render();
|
|
||||||
} catch (e) {
|
|
||||||
toastr.error('Error instalando archivo: ' + (e && e.message ? e.message : e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TS_loadInstalledAppsFromDB().then(() => {
|
|
||||||
TS_loadExternalAppsFromDB().then(() => {
|
|
||||||
render();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
edit: function () {
|
|
||||||
this.index();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user