Refactor code to use single quotes for strings, update HTML structure for better readability, and improve error handling in various modules. Added Prettier configuration for consistent code formatting.

This commit is contained in:
naielv
2026-02-05 23:07:35 +01:00
parent 6d7def5f18
commit 8a9fee46da
39 changed files with 3765 additions and 8749 deletions

View File

@@ -94,8 +94,6 @@ TeleSec is a Spanish Progressive Web Application (PWA) built with vanilla JavaSc
├── manifest.json # PWA manifest
├── *.png, *.jpg # Icons and images
├── static/ # JavaScript libraries and CSS
│ │ ├── pouchdb (via CDN) # PouchDB is used for local storage and replication
│ ├── webrtc.js # WebRTC functionality
│ ├── euskaditech-css/ # CSS framework
│ └── ico/ # Application icons
└── page/ # Page-specific assets (empty placeholder)

7
.prettierrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"printWidth": 100,
"singleQuote": true,
"semi": true,
"trailingComma": "es5",
"embeddedLanguageFormatting": "auto"
}

View File

@@ -1,109 +0,0 @@
;(function(){
var sT = setTimeout || {}, u;
if(typeof window !== ''+u){ sT.window = window }
var AXE = (sT.window||'').AXE || function(){};
if(AXE.window = sT.window){ AXE.window.AXE = AXE }
var Gun = (AXE.window||'').GUN || require('./gun');
(Gun.AXE = AXE).GUN = AXE.Gun = Gun;
//if(!Gun.window){ try{ require('./lib/axe') }catch(e){} }
if(!Gun.window){ require('./lib/axe') }
Gun.on('opt', function(at){ start(at) ; this.to.next(at) }); // make sure to call the "next" middleware adapter.
function start(root){
if(root.axe){ return }
var opt = root.opt, peers = opt.peers;
if(false === opt.axe){ return }
if(!Gun.window){ return } // handled by ^ lib/axe.js
var w = Gun.window, lS = w.localStorage || opt.localStorage || {}, loc = w.location || opt.location || {}, nav = w.navigator || opt.navigator || {};
var axe = root.axe = {}, tmp, id;
var mesh = opt.mesh = opt.mesh || Gun.Mesh(root); // DAM!
tmp = peers[id = loc.origin + '/gun'] = peers[id] || {};
tmp.id = tmp.url = id; tmp.retry = tmp.retry || 0;
tmp = peers[id = 'http://localhost:8765/gun'] = peers[id] || {};
tmp.id = tmp.url = id; tmp.retry = tmp.retry || 0;
Gun.log.once("AXE", "AXE enabled: Trying to find network via (1) local peer (2) last used peers (3) a URL parameter, and last (4) hard coded peers.");
Gun.log.once("AXEWarn", "Warning: AXE is in alpha, use only for testing!");
var last = lS.peers || ''; if(last){ last += ' ' }
last += ((loc.search||'').split('peers=')[1]||'').split('&')[0];
root.on('bye', function(peer){
this.to.next(peer);
if(!peer.url){ return } // ignore WebRTC disconnects for now.
if(!nav.onLine){ peer.retry = 1 }
if(peer.retry){ return }
if(axe.fall){ delete axe.fall[peer.url || peer.id] }
(function next(){
if(!axe.fall){ setTimeout(next, 9); return } // not found yet
var fall = Object.keys(axe.fall||''), one = fall[(Math.random()*fall.length) >> 0];
if(!fall.length){ lS.peers = ''; one = 'https://gunjs.herokuapp.com/gun' } // out of peers
if(peers[one]){ next(); return } // already choose
mesh.hi(one);
}());
});
root.on('hi', function(peer){ // TEMPORARY! Try to connect all peers.
this.to.next(peer);
if(!peer.url){ return } // ignore WebRTC disconnects for now.
return; // DO NOT COMMIT THIS FEATURE YET! KEEP TESTING NETWORK PERFORMANCE FIRST!
(function next(){
if(!peer.wire){ return }
if(!axe.fall){ setTimeout(next, 9); return } // not found yet
var one = (next.fall = next.fall || Object.keys(axe.fall||'')).pop();
if(!one){ return }
setTimeout(next, 99);
mesh.say({dam: 'opt', opt: {peers: one}}, peer);
}());
});
function found(text){
axe.fall = {};
((text||'').match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/ig)||[]).forEach(function(url){
axe.fall[url] = {url: url, id: url, retry: 0}; // RETRY
});
return;
// TODO: Finish porting below? Maybe not.
Object.keys(last.peers||'').forEach(function(key){
tmp = peers[id = key] = peers[id] || {};
tmp.id = tmp.url = id;
});
tmp = peers[id = 'https://guntest.herokuapp.com/gun'] = peers[id] || {};
tmp.id = tmp.url = id;
var mesh = opt.mesh = opt.mesh || Gun.Mesh(root); // DAM!
mesh.way = function(msg){
if(root.$ === msg.$ || (msg._||'').via){
mesh.say(msg, opt.peers);
return;
}
var at = (msg.$||'')._;
if(!at){ mesh.say(msg, opt.peers); return }
if(msg.get){
if(at.axe){ return } // don't ask for it again!
at.axe = {};
}
mesh.say(msg, opt.peers);
}
}
if(last){ found(last); return }
try{ fetch(((loc.search||'').split('axe=')[1]||'').split('&')[0] || loc.axe || 'https://raw.githubusercontent.com/wiki/amark/gun/volunteer.dht.md').then(function(res){
return res.text()
}).then(function(text){
found(lS.peers = text);
}).catch(function(){
found(); // nothing
})}catch(e){found()}
}
var empty = {}, yes = true;
try{ if(typeof module != ''+u){ module.exports = AXE } }catch(e){}
}());

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
Gun.chain.open || require('./open');
Gun.chain.load = function(cb, opt, at){
(opt = opt || {}).off = !0;
return this.open(cb, opt, at);
}

View File

@@ -1,60 +0,0 @@
// assets/static/open.js - Deprecated. Part of Gun library, not used after migration to PouchDB.
console.warn('assets/static/open.js is deprecated and unused.');
var Gun = (typeof window !== "undefined")? window.Gun || {} : {};
Gun.chain.open = function(cb, opt, at, depth){ // this is a recursive function, BEWARE!
depth = depth || 1;
opt = opt || {}; // init top level options.
opt.doc = opt.doc || {};
opt.ids = opt.ids || {};
opt.any = opt.any || cb;
opt.meta = opt.meta || false;
opt.eve = opt.eve || {off: function(){ // collect all recursive events to unsubscribe to if needed.
Object.keys(opt.eve.s).forEach(function(i,e){ // switch to CPU scheduled setTimeout.each?
if(e = opt.eve.s[i]){ e.off() }
});
opt.eve.s = {};
}, s:{}}
return this.on(function(data, key, ctx, eve){ // subscribe to 1 deeper of data!
clearTimeout(opt.to); // do not trigger callback if bunch of changes...
opt.to = setTimeout(function(){ // but schedule the callback to fire soon!
if(!opt.any){ return }
opt.any.call(opt.at.$, opt.doc, opt.key, opt, opt.eve); // call it.
if(opt.off){ // check for unsubscribing.
opt.eve.off();
opt.any = null;
}
}, opt.wait || 9);
opt.at = opt.at || ctx; // opt.at will always be the first context it finds.
opt.key = opt.key || key;
opt.eve.s[this._.id] = eve; // collect all the events together.
if(true === Gun.valid(data)){ // if primitive value...
if(!at){
opt.doc = data;
} else {
at[key] = data;
}
return;
}
var tmp = this; // else if a sub-object, CPU schedule loop over properties to do recursion.
setTimeout.each(Object.keys(data), function(key, val){
if('_' === key && !opt.meta){ return }
val = data[key];
var doc = at || opt.doc, id; // first pass this becomes the root of open, then at is passed below, and will be the parent for each sub-document/object.
if(!doc){ return } // if no "parent"
if('string' !== typeof (id = Gun.valid(val))){ // if primitive...
doc[key] = val;
return;
}
if(opt.ids[id]){ // if we've already seen this sub-object/document
doc[key] = opt.ids[id]; // link to itself, our already in-memory one, not a new copy.
return;
}
if(opt.depth <= depth){ // stop recursive open at max depth.
doc[key] = doc[key] || val; // show link so app can load it if need.
return;
} // now open up the recursion of sub-documents!
tmp.get(key).open(opt.any, opt, opt.ids[id] = doc[key] = {}, depth+1); // 3rd param is now where we are "at".
});
})
}

View File

@@ -1,31 +0,0 @@
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
Gun.chain.path = function(field, opt){
var back = this, gun = back, tmp;
if(typeof field === 'string'){
tmp = field.split(opt || '.');
if(1 === tmp.length){
gun = back.get(field);
return gun;
}
field = tmp;
}
if(field instanceof Array){
if(field.length > 1){
gun = back;
var i = 0, l = field.length;
for(i; i < l; i++){
//gun = gun.get(field[i], (i+1 === l)? cb : null, opt);
gun = gun.get(field[i]);
}
} else {
gun = back.get(field[0]);
}
return gun;
}
if(!field && 0 != field){
return back;
}
gun = back.get(''+field);
return gun;
}

View File

@@ -1,606 +0,0 @@
;(function(){
function Radisk(opt){
opt = opt || {};
opt.log = opt.log || console.log;
opt.file = String(opt.file || 'radata');
var has = (Radisk.has || (Radisk.has = {}))[opt.file];
if(has){ return has }
opt.max = opt.max || (opt.memory? (opt.memory * 999 * 999) : 300000000) * 0.3;
opt.until = opt.until || opt.wait || 250;
opt.batch = opt.batch || (10 * 1000);
opt.chunk = opt.chunk || (1024 * 1024 * 1); // 1MB
opt.code = opt.code || {};
opt.code.from = opt.code.from || '!';
opt.jsonify = true;
function ename(t){ return encodeURIComponent(t).replace(/\*/g, '%2A') } // TODO: Hash this also, but allow migration!
function atomic(v){ return u !== v && (!v || 'object' != typeof v) }
var timediate = (''+u === typeof setImmediate)? setTimeout : setImmediate;
var puff = setTimeout.turn || timediate, u;
var map = Radix.object;
var ST = 0;
if(!opt.store){
return opt.log("ERROR: Radisk needs `opt.store` interface with `{get: fn, put: fn (, list: fn)}`!");
}
if(!opt.store.put){
return opt.log("ERROR: Radisk needs `store.put` interface with `(file, data, cb)`!");
}
if(!opt.store.get){
return opt.log("ERROR: Radisk needs `store.get` interface with `(file, cb)`!");
}
if(!opt.store.list){
//opt.log("WARNING: `store.list` interface might be needed!");
}
if(''+u != typeof require){ require('./yson') }
var parse = JSON.parseAsync || function(t,cb,r){ var u; try{ cb(u, JSON.parse(t,r)) }catch(e){ cb(e) } }
var json = JSON.stringifyAsync || function(v,cb,r,s){ var u; try{ cb(u, JSON.stringify(v,r,s)) }catch(e){ cb(e) } }
/*
Any and all storage adapters should...
1. Because writing to disk takes time, we should batch data to disk. This improves performance, and reduces potential disk corruption.
2. If a batch exceeds a certain number of writes, we should immediately write to disk when physically possible. This caps total performance, but reduces potential loss.
*/
var r = function(key, data, cb, tag, DBG){
if('function' === typeof data){
var o = cb || {};
cb = data;
r.read(key, cb, o, DBG || tag);
return;
}
//var tmp = (tmp = r.batch = r.batch || {})[key] = tmp[key] || {};
//var tmp = (tmp = r.batch = r.batch || {})[key] = data;
r.save(key, data, cb, tag, DBG);
}
r.save = function(key, data, cb, tag, DBG){
var s = {key: key}, tags, f, d, q;
s.find = function(file){ var tmp;
s.file = file || (file = opt.code.from);
DBG && (DBG = DBG[file] = DBG[file] || {});
DBG && (DBG.sf = DBG.sf || +new Date);
//console.only.i && console.log('found', file);
if(tmp = r.disk[file]){ s.mix(u, tmp); return }
r.parse(file, s.mix, u, DBG);
}
s.mix = function(err, disk){
DBG && (DBG.sml = +new Date);
DBG && (DBG.sm = DBG.sm || +new Date);
if(s.err = err || s.err){ cb(err); return } // TODO: HANDLE BATCH EMIT
var file = s.file = (disk||'').file || s.file, tmp;
if(!disk && file !== opt.code.from){ // corrupt file?
r.find.bad(file); // remove from dir list
r.save(key, data, cb, tag); // try again
return;
}
(disk = r.disk[file] || (r.disk[file] = disk || Radix())).file || (disk.file = file);
if(opt.compare){
data = opt.compare(disk(key), data, key, file);
if(u === data){ cb(err, -1); return } // TODO: HANDLE BATCH EMIT
}
(s.disk = disk)(key, data);
if(tag){
(tmp = (tmp = disk.tags || (disk.tags = {}))[tag] || (tmp[tag] = r.tags[tag] || (r.tags[tag] = {})))[file] || (tmp[file] = r.one[tag] || (r.one[tag] = cb));
cb = null;
}
DBG && (DBG.st = DBG.st || +new Date);
//console.only.i && console.log('mix', disk.Q);
if(disk.Q){ cb && disk.Q.push(cb); return } disk.Q = (cb? [cb] : []);
disk.to = setTimeout(s.write, opt.until);
}
s.write = function(){
DBG && (DBG.sto = DBG.sto || +new Date);
var file = f = s.file, disk = d = s.disk;
q = s.q = disk.Q;
tags = s.tags = disk.tags;
delete disk.Q;
delete r.disk[file];
delete disk.tags;
//console.only.i && console.log('write', file, disk, 'was saving:', key, data);
r.write(file, disk, s.ack, u, DBG);
}
s.ack = function(err, ok){
DBG && (DBG.sa = DBG.sa || +new Date);
DBG && (DBG.sal = q.length);
var ack, tmp;
// TODO!!!! CHANGE THIS INTO PUFF!!!!!!!!!!!!!!!!
for(var id in r.tags){
if(!r.tags.hasOwnProperty(id)){ continue } var tag = r.tags[id];
if((tmp = r.disk[f]) && (tmp = tmp.tags) && tmp[tag]){ continue }
ack = tag[f];
delete tag[f];
var ne; for(var k in tag){ if(tag.hasOwnProperty(k)){ ne = true; break } } // is not empty?
if(ne){ continue } //if(!obj_empty(tag)){ continue }
delete r.tags[tag];
ack && ack(err, ok);
}
!q && (q = '');
var l = q.length, i = 0;
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
var S = +new Date;
for(;i < l; i++){ (ack = q[i]) && ack(err, ok) }
console.STAT && console.STAT(S, +new Date - S, 'rad acks', ename(s.file));
console.STAT && console.STAT(S, q.length, 'rad acks #', ename(s.file));
}
cb || (cb = function(err, ok){ // test delete!
if(!err){ return }
});
//console.only.i && console.log('save', key);
r.find(key, s.find);
}
r.disk = {};
r.one = {};
r.tags = {};
/*
Any storage engine at some point will have to do a read in order to write.
This is true of even systems that use an append only log, if they support updates.
Therefore it is unavoidable that a read will have to happen,
the question is just how long you delay it.
*/
var RWC = 0;
r.write = function(file, rad, cb, o, DBG){
if(!rad){ cb('No radix!'); return }
o = ('object' == typeof o)? o : {force: o};
var f = function Fractal(){}, a, b;
f.text = '';
f.file = file = rad.file || (rad.file = file);
if(!file){ cb('What file?'); return }
f.write = function(){
var text = rad.raw = f.text;
r.disk[file = rad.file || f.file || file] = rad;
var S = +new Date;
DBG && (DBG.wd = S);
//console.only.i && console.log('add', file);
r.find.add(file, function add(err){
DBG && (DBG.wa = +new Date);
if(err){ cb(err); return }
//console.only.i && console.log('disk', file, text);
opt.store.put(ename(file), text, function safe(err, ok){
DBG && (DBG.wp = +new Date);
console.STAT && console.STAT(S, ST = +new Date - S, "wrote disk", JSON.stringify(file), ++RWC, 'total all writes.');
//console.only.i && console.log('done', err, ok || 1, cb);
cb(err, ok || 1);
if(!rad.Q){ delete r.disk[file] } // VERY IMPORTANT! Clean up memory, but not if there is already queued writes on it!
});
});
}
f.split = function(){
var S = +new Date;
DBG && (DBG.wf = S);
f.text = '';
if(!f.count){ f.count = 0;
Radix.map(rad, function count(){ f.count++ }); // TODO: Perf? Any faster way to get total length?
}
DBG && (DBG.wfc = f.count);
f.limit = Math.ceil(f.count/2);
var SC = f.count;
f.count = 0;
DBG && (DBG.wf1 = +new Date);
f.sub = Radix();
Radix.map(rad, f.slice, {reverse: 1}); // IMPORTANT: DO THIS IN REVERSE, SO LAST HALF OF DATA MOVED TO NEW FILE BEFORE DROPPING FROM CURRENT FILE.
DBG && (DBG.wf2 = +new Date);
r.write(f.end, f.sub, f.both, o);
DBG && (DBG.wf3 = +new Date);
f.hub = Radix();
Radix.map(rad, f.stop);
DBG && (DBG.wf4 = +new Date);
r.write(rad.file, f.hub, f.both, o);
DBG && (DBG.wf5 = +new Date);
console.STAT && console.STAT(S, +new Date - S, "rad split", ename(rad.file), SC);
return true;
}
f.slice = function(val, key){
f.sub(f.end = key, val);
if(f.limit <= (++f.count)){ return true }
}
f.stop = function(val, key){
if(key >= f.end){ return true }
f.hub(key, val);
}
f.both = function(err, ok){
DBG && (DBG.wfd = +new Date);
if(b){ cb(err || b); return }
if(a){ cb(err, ok); return }
a = true;
b = err;
}
f.each = function(val, key, k, pre){
if(u !== val){ f.count++ }
if(opt.max <= (val||'').length){ return cb("Data too big!"), true }
var enc = Radisk.encode(pre.length) +'#'+ Radisk.encode(k) + (u === val? '' : ':'+ Radisk.encode(val)) +'\n';
if((opt.chunk < f.text.length + enc.length) && (1 < f.count) && !o.force){
return f.split();
}
f.text += enc;
}
//console.only.i && console.log('writing');
if(opt.jsonify){ r.write.jsonify(f, rad, cb, o, DBG); return } // temporary testing idea
if(!Radix.map(rad, f.each, true)){ f.write() }
}
r.write.jsonify = function(f, rad, cb, o, DBG){
var raw;
var S = +new Date;
DBG && (DBG.w = S);
try{raw = JSON.stringify(rad.$);
}catch(e){ cb("Cannot radisk!"); return }
DBG && (DBG.ws = +new Date);
console.STAT && console.STAT(S, +new Date - S, "rad stringified JSON");
if(opt.chunk < raw.length && !o.force){
var c = 0;
Radix.map(rad, function(){
if(c++){ return true } // more than 1 item
});
if(c > 1){
return f.split();
}
}
f.text = raw;
f.write();
}
r.range = function(tree, o){
if(!tree || !o){ return }
if(u === o.start && u === o.end){ return tree }
if(atomic(tree)){ return tree }
var sub = Radix();
Radix.map(tree, function(v,k){ sub(k,v) }, o); // ONLY PLACE THAT TAKES TREE, maybe reduce API for better perf?
return sub('');
}
;(function(){
r.read = function(key, cb, o, DBG){
o = o || {};
var g = {key: key};
g.find = function(file){ var tmp;
g.file = file || (file = opt.code.from);
DBG && (DBG = DBG[file] = DBG[file] || {});
DBG && (DBG.rf = DBG.rf || +new Date);
if(tmp = r.disk[g.file = file]){ g.check(u, tmp); return }
r.parse(file, g.check, u, DBG);
}
g.get = function(err, disk, info){
DBG && (DBG.rgl = +new Date);
DBG && (DBG.rg = DBG.rg || +new Date);
if(g.err = err || g.err){ cb(err); return }
var file = g.file = (disk||'').file || g.file;
if(!disk && file !== opt.code.from){ // corrupt file?
r.find.bad(file); // remove from dir list
r.read(key, cb, o); // try again
return;
}
disk = r.disk[file] || (r.disk[file] = disk);
if(!disk){ cb(file === opt.code.from? u : "No file!"); return }
disk.file || (disk.file = file);
var data = r.range(disk(key), o);
DBG && (DBG.rr = +new Date);
o.unit = disk.unit;
o.chunks = (o.chunks || 0) + 1;
o.parsed = (o.parsed || 0) + ((info||'').parsed||(o.chunks*opt.chunk));
o.more = 1;
o.next = u;
Radix.map(r.list, function next(v,f){
if(!v || file === f){ return }
o.next = f;
return 1;
}, o.reverse? {reverse: 1, end: file} : {start: file});
DBG && (DBG.rl = +new Date);
if(!o.next){ o.more = 0 }
if(o.next){
if(!o.reverse && ((key < o.next && 0 != o.next.indexOf(key)) || (u !== o.end && (o.end || '\uffff') < o.next))){ o.more = 0 }
if(o.reverse && ((key > o.next && 0 != key.indexOf(o.next)) || ((u !== o.start && (o.start || '') > o.next && file <= o.start)))){ o.more = 0 }
}
//console.log(5, process.memoryUsage().heapUsed);
if(!o.more){ cb(g.err, data, o); return }
if(data){ cb(g.err, data, o) }
if(o.parsed >= o.limit){ return }
var S = +new Date;
DBG && (DBG.rm = S);
var next = o.next;
timediate(function(){
console.STAT && console.STAT(S, +new Date - S, 'rad more');
r.parse(next, g.check);
},0);
}
g.check = function(err, disk, info){
//console.log(4, process.memoryUsage().heapUsed);
g.get(err, disk, info);
if(!disk || disk.check){ return } disk.check = 1;
var S = +new Date;
(info || (info = {})).file || (info.file = g.file);
Radix.map(disk, function(val, key){
// assume in memory for now, since both write/read already call r.find which will init it.
r.find(key, function(file){
if((file || (file = opt.code.from)) === info.file){ return }
var id = (''+Math.random()).slice(-3);
puff(function(){
r.save(key, val, function ack(err, ok){
if(err){ r.save(key, val, ack); return } // ad infinitum???
// TODO: NOTE!!! Mislocated data could be because of a synchronous `put` from the `g.get(` other than perf shouldn't we do the check first before acking?
console.STAT && console.STAT("MISLOCATED DATA CORRECTED", id, ename(key), ename(info.file), ename(file));
});
},0);
})
});
console.STAT && console.STAT(S, +new Date - S, "rad check");
}
r.find(key || (o.reverse? (o.end||'') : (o.start||'')), g.find);
}
function rev(a,b){ return b }
var revo = {reverse: true};
}());
;(function(){
/*
Let us start by assuming we are the only process that is
changing the directory or bucket. Not because we do not want
to be multi-process/machine, but because we want to experiment
with how much performance and scale we can get out of only one.
Then we can work on the harder problem of being multi-process.
*/
var RPC = 0;
var Q = {}, s = String.fromCharCode(31);
r.parse = function(file, cb, raw, DBG){ var q;
if(!file){ return cb(); }
if(q = Q[file]){ q.push(cb); return } q = Q[file] = [cb];
var p = function Parse(){}, info = {file: file};
(p.disk = Radix()).file = file;
p.read = function(err, data){ var tmp;
DBG && (DBG.rpg = +new Date);
console.STAT && console.STAT(S, +new Date - S, 'read disk', JSON.stringify(file), ++RPC, 'total all parses.');
//console.log(2, process.memoryUsage().heapUsed);
if((p.err = err) || (p.not = !data)){
delete Q[file];
p.map(q, p.ack);
return;
}
if('string' !== typeof data){
try{
if(opt.max <= data.length){
p.err = "Chunk too big!";
} else {
data = data.toString(); // If it crashes, it crashes here. How!?? We check size first!
}
}catch(e){ p.err = e }
if(p.err){
delete Q[file];
p.map(q, p.ack);
return;
}
}
info.parsed = data.length;
DBG && (DBG.rpl = info.parsed);
DBG && (DBG.rpa = q.length);
S = +new Date;
if(!(opt.jsonify || '{' === data[0])){
p.radec(err, data);
return;
}
parse(data, function(err, tree){
//console.log(3, process.memoryUsage().heapUsed);
if(!err){
delete Q[file];
p.disk.$ = tree;
console.STAT && (ST = +new Date - S) > 9 && console.STAT(S, ST, 'rad parsed JSON');
DBG && (DBG.rpd = +new Date);
p.map(q, p.ack); // hmmm, v8 profiler can't see into this cause of try/catch?
return;
}
if('{' === data[0]){
delete Q[file];
p.err = tmp || "JSON error!";
p.map(q, p.ack);
return;
}
p.radec(err, data);
});
}
p.map = function(){ // switch to setTimeout.each now?
if(!q || !q.length){ return }
//var i = 0, l = q.length, ack;
var S = +new Date;
var err = p.err, data = p.not? u : p.disk;
var i = 0, ack; while(i < 9 && (ack = q[i++])){ ack(err, data, info) } // too much?
console.STAT && console.STAT(S, +new Date - S, 'rad packs', ename(file));
console.STAT && console.STAT(S, i, 'rad packs #', ename(file));
if(!(q = q.slice(i)).length){ return }
puff(p.map, 0);
}
p.ack = function(cb){
if(!cb){ return }
if(p.err || p.not){
cb(p.err, u, info);
return;
}
cb(u, p.disk, info);
}
p.radec = function(err, data){
delete Q[file];
S = +new Date;
var tmp = p.split(data), pre = [], i, k, v;
if(!tmp || 0 !== tmp[1]){
p.err = "File '"+file+"' does not have root radix! ";
p.map(q, p.ack);
return;
}
while(tmp){
k = v = u;
i = tmp[1];
tmp = p.split(tmp[2])||'';
if('#' == tmp[0]){
k = tmp[1];
pre = pre.slice(0,i);
if(i <= pre.length){
pre.push(k);
}
}
tmp = p.split(tmp[2])||'';
if('\n' == tmp[0]){ continue }
if('=' == tmp[0] || ':' == tmp[0]){ v = tmp[1] }
if(u !== k && u !== v){ p.disk(pre.join(''), v) }
tmp = p.split(tmp[2]);
}
console.STAT && console.STAT(S, +new Date - S, 'parsed RAD');
p.map(q, p.ack);
};
p.split = function(t){
if(!t){ return }
var l = [], o = {}, i = -1, a = '', b, c;
i = t.indexOf(s);
if(!t[i]){ return }
a = t.slice(0, i);
l[0] = a;
l[1] = b = Radisk.decode(t.slice(i), o);
l[2] = t.slice(i + o.i);
return l;
}
if(r.disk){ raw || (raw = (r.disk[file]||'').raw) }
var S = +new Date, SM, SL;
DBG && (DBG.rp = S);
if(raw){ return puff(function(){ p.read(u, raw) }, 0) }
opt.store.get(ename(file), p.read);
// TODO: What if memory disk gets filled with updates, and we get an old one back?
}
}());
;(function(){
var dir, f = String.fromCharCode(28), Q;
r.find = function(key, cb){
if(!dir){
if(Q){ Q.push([key, cb]); return } Q = [[key, cb]];
r.parse(f, init);
return;
}
Radix.map(r.list = dir, function(val, key){
if(!val){ return }
return cb(key) || true;
}, {reverse: 1, end: key}) || cb(opt.code.from);
}
r.find.add = function(file, cb){
var has = dir(file);
if(has || file === f){ cb(u, 1); return }
dir(file, 1);
cb.found = (cb.found || 0) + 1;
r.write(f, dir, function(err, ok){
if(err){ cb(err); return }
cb.found = (cb.found || 0) - 1;
if(0 !== cb.found){ return }
cb(u, 1);
}, true);
}
r.find.bad = function(file, cb){
dir(file, 0);
r.write(f, dir, cb||noop);
}
function init(err, disk){
if(err){
opt.log('list', err);
setTimeout(function(){ r.parse(f, init) }, 1000);
return;
}
if(disk){ drain(disk); return }
dir = dir || disk || Radix();
if(!opt.store.list){ drain(dir); return }
// import directory.
opt.store.list(function(file){
if(!file){ drain(dir); return }
r.find.add(file, noop);
});
}
function drain(rad, tmp){
dir = dir || rad;
dir.file = f;
tmp = Q; Q = null;
map(tmp, function(arg){
r.find(arg[0], arg[1]);
});
}
}());
try{ !Gun.window && require('./radmigtmp')(r) }catch(e){}
var noop = function(){}, RAD, u;
Radisk.has[opt.file] = r;
return r;
}
;(function(){
var _ = String.fromCharCode(31), u;
Radisk.encode = function(d, o, s){ s = s || _;
var t = s, tmp;
if(typeof d == 'string'){
var i = d.indexOf(s);
while(i != -1){ t += s; i = d.indexOf(s, i+1) }
return t + '"' + d + s;
} else
if(d && d['#'] && 1 == Object.keys(d).length){
return t + '#' + tmp + t;
} else
if('number' == typeof d){
return t + '+' + (d||0) + t;
} else
if(null === d){
return t + ' ' + t;
} else
if(true === d){
return t + '+' + t;
} else
if(false === d){
return t + '-' + t;
}// else
//if(binary){}
}
Radisk.decode = function(t, o, s){ s = s || _;
var d = '', i = -1, n = 0, c, p;
if(s !== t[0]){ return }
while(s === t[++i]){ ++n }
p = t[c = n] || true;
while(--n >= 0){ i = t.indexOf(s, i+1) }
if(i == -1){ i = t.length }
d = t.slice(c+1, i);
if(o){ o.i = i+1 }
if('"' === p){
return d;
} else
if('#' === p){
return {'#':d};
} else
if('+' === p){
if(0 === d.length){
return true;
}
return parseFloat(d);
} else
if(' ' === p){
return null;
} else
if('-' === p){
return false;
}
}
}());
if(typeof window !== "undefined"){
var Gun = window.Gun;
var Radix = window.Radix;
window.Radisk = Radisk;
} else {
var Gun = require('../gun');
var Radix = require('./radix');
//var Radix = require('./radix2'); Radisk = require('./radisk2');
try{ module.exports = Radisk }catch(e){}
}
Radisk.Radix = Radix;
}());

View File

@@ -1,124 +0,0 @@
;(function(){
function Radix(){
var radix = function(key, val, t){
radix.unit = 0;
if(!t && u !== val){
radix.last = (''+key < radix.last)? radix.last : ''+key;
delete (radix.$||{})[_];
}
t = t || radix.$ || (radix.$ = {});
if(!key && Object.keys(t).length){ return t }
key = ''+key;
var i = 0, l = key.length-1, k = key[i], at, tmp;
while(!(at = t[k]) && i < l){
k += key[++i];
}
if(!at){
if(!each(t, function(r, s){
var ii = 0, kk = '';
if((s||'').length){ while(s[ii] == key[ii]){
kk += s[ii++];
} }
if(kk){
if(u === val){
if(ii <= l){ return }
(tmp || (tmp = {}))[s.slice(ii)] = r;
//(tmp[_] = function $(){ $.sort = Object.keys(tmp).sort(); return $ }()); // get rid of this one, cause it is on read?
return r;
}
var __ = {};
__[s.slice(ii)] = r;
ii = key.slice(ii);
('' === ii)? (__[''] = val) : ((__[ii] = {})[''] = val);
//(__[_] = function $(){ $.sort = Object.keys(__).sort(); return $ }());
t[kk] = __;
if(Radix.debug && 'undefined' === ''+kk){ console.log(0, kk); debugger }
delete t[s];
//(t[_] = function $(){ $.sort = Object.keys(t).sort(); return $ }());
return true;
}
})){
if(u === val){ return; }
(t[k] || (t[k] = {}))[''] = val;
if(Radix.debug && 'undefined' === ''+k){ console.log(1, k); debugger }
//(t[_] = function $(){ $.sort = Object.keys(t).sort(); return $ }());
}
if(u === val){
return tmp;
}
} else
if(i == l){
//if(u === val){ return (u === (tmp = at['']))? at : tmp } // THIS CODE IS CORRECT, below is
if(u === val){ return (u === (tmp = at['']))? at : ((radix.unit = 1) && tmp) } // temporary help??
at[''] = val;
//(at[_] = function $(){ $.sort = Object.keys(at).sort(); return $ }());
} else {
if(u !== val){ delete at[_] }
//at && (at[_] = function $(){ $.sort = Object.keys(at).sort(); return $ }());
return radix(key.slice(++i), val, at || (at = {}));
}
}
return radix;
};
Radix.map = function rap(radix, cb, opt, pre){
try {
pre = pre || []; // TODO: BUG: most out-of-memory crashes come from here.
var t = ('function' == typeof radix)? radix.$ || {} : radix;
//!opt && console.log("WHAT IS T?", JSON.stringify(t).length);
if(!t){ return }
if('string' == typeof t){ if(Radix.debug){ throw ['BUG:', radix, cb, opt, pre] } return; }
var keys = (t[_]||no).sort || (t[_] = function $(){ $.sort = Object.keys(t).sort(); return $ }()).sort, rev; // ONLY 17% of ops are pre-sorted!
//var keys = Object.keys(t).sort();
opt = (true === opt)? {branch: true} : (opt || {});
if(rev = opt.reverse){ keys = keys.slice(0).reverse() }
var start = opt.start, end = opt.end, END = '\uffff';
var i = 0, l = keys.length;
for(;i < l; i++){ var key = keys[i], tree = t[key], tmp, p, pt;
if(!tree || '' === key || _ === key || 'undefined' === key){ continue }
p = pre.slice(0); p.push(key);
pt = p.join('');
if(u !== start && pt < (start||'').slice(0,pt.length)){ continue }
if(u !== end && (end || END) < pt){ continue }
if(rev){ // children must be checked first when going in reverse.
tmp = rap(tree, cb, opt, p);
if(u !== tmp){ return tmp }
}
if(u !== (tmp = tree[''])){
var yes = 1;
if(u !== start && pt < (start||'')){ yes = 0 }
if(u !== end && pt > (end || END)){ yes = 0 }
if(yes){
tmp = cb(tmp, pt, key, pre);
if(u !== tmp){ return tmp }
}
} else
if(opt.branch){
tmp = cb(u, pt, key, pre);
if(u !== tmp){ return tmp }
}
pre = p;
if(!rev){
tmp = rap(tree, cb, opt, pre);
if(u !== tmp){ return tmp }
}
pre.pop();
}
} catch (e) { console.error(e); }
};
if(typeof window !== "undefined"){
window.Radix = Radix;
} else {
try{ module.exports = Radix }catch(e){}
}
var each = Radix.object = function(o, f, r){
for(var k in o){
if(!o.hasOwnProperty(k)){ continue }
if((r = f(o[k], k)) !== u){ return r }
}
}, no = {}, u;
var _ = String.fromCharCode(24);
}());

View File

@@ -1,79 +0,0 @@
;(function(){
/* // from @jabis
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
// quota.usage -> Number of bytes used.
// quota.quota -> Maximum number of bytes available.
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(`You've used ${percentageUsed}% of the available storage.`);
const remaining = quota.quota - quota.usage;
console.log(`You can write up to ${remaining} more bytes.`);
}
*/
function Store(opt){
opt = opt || {};
opt.file = String(opt.file || 'radata');
var store = Store[opt.file], db = null, u;
if(store){
console.log("Warning: reusing same IndexedDB store and options as 1st.");
return Store[opt.file];
}
store = Store[opt.file] = function(){};
try{opt.indexedDB = opt.indexedDB || Store.indexedDB || indexedDB}catch(e){}
try{if(!opt.indexedDB || 'file:' == location.protocol){
var s = store.d || (store.d = {});
store.put = function(f, d, cb){ s[f] = d; setTimeout(function(){ cb(null, 1) },250) };
store.get = function(f, cb){ setTimeout(function(){ cb(null, s[f] || u) },5) };
console.log('Warning: No indexedDB exists to persist data to!');
return store;
}}catch(e){}
store.start = function(){
var o = indexedDB.open(opt.file, 1);
o.onupgradeneeded = function(eve){ (eve.target.result).createObjectStore(opt.file) }
o.onsuccess = function(){ db = o.result }
o.onerror = function(eve){ console.log(eve||1); }
}; store.start();
store.put = function(key, data, cb){
if(!db){ setTimeout(function(){ store.put(key, data, cb) },1); return }
var tx = db.transaction([opt.file], 'readwrite');
var obj = tx.objectStore(opt.file);
var req = obj.put(data, ''+key);
req.onsuccess = obj.onsuccess = tx.onsuccess = function(){ cb(null, 1) }
req.onabort = obj.onabort = tx.onabort = function(eve){ cb(eve||'put.tx.abort') }
req.onerror = obj.onerror = tx.onerror = function(eve){ cb(eve||'put.tx.error') }
}
store.get = function(key, cb){
if(!db){ setTimeout(function(){ store.get(key, cb) },9); return }
var tx = db.transaction([opt.file], 'readonly');
var obj = tx.objectStore(opt.file);
var req = obj.get(''+key);
req.onsuccess = function(){ cb(null, req.result) }
req.onabort = function(eve){ cb(eve||4) }
req.onerror = function(eve){ cb(eve||5) }
}
setInterval(function(){ db && db.close(); db = null; store.start() }, 1000 * 15); // reset webkit bug?
return store;
}
if(typeof window !== "undefined"){
(Store.window = window).RindexedDB = Store;
Store.indexedDB = window.indexedDB; // safari bug
} else {
try{ module.exports = Store }catch(e){}
}
try{
var Gun = Store.window.Gun || require('../gun');
Gun.on('create', function(root){
this.to.next(root);
root.opt.store = root.opt.store || Store(root.opt);
});
}catch(e){}
}());

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,150 +0,0 @@
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
Gun.on('create', function(root){
if(Gun.TESTING){ root.opt.file = 'radatatest' }
this.to.next(root);
var opt = root.opt, empty = {}, u;
if(false === opt.rad || false === opt.radisk){ return }
if((u+'' != typeof process) && 'false' === ''+(process.env||'').RAD){ return }
var Radisk = (Gun.window && Gun.window.Radisk) || require('./radisk');
var Radix = Radisk.Radix;
var dare = Radisk(opt), esc = String.fromCharCode(27);
var ST = 0;
root.on('put', function(msg){
this.to.next(msg);
if((msg._||'').rad){ return } // don't save what just came from a read.
//if(msg['@']){ return } // WHY DID I NOT ADD THIS?
var id = msg['#'], put = msg.put, soul = put['#'], key = put['.'], val = put[':'], state = put['>'], tmp;
var DBG = (msg._||'').DBG; DBG && (DBG.sp = DBG.sp || +new Date);
//var lot = (msg._||'').lot||''; count[id] = (count[id] || 0) + 1;
var S = (msg._||'').RPS || ((msg._||'').RPS = +new Date);
//console.log("PUT ------->>>", soul,key, val, state);
//dare(soul+esc+key, {':': val, '>': state}, dare.one[id] || function(err, ok){
dare(soul+esc+key, {':': val, '>': state}, function(err, ok){
//console.log("<<<------- PAT", soul,key, val, state, 'in', +new Date - S);
DBG && (DBG.spd = DBG.spd || +new Date);
console.STAT && console.STAT(S, +new Date - S, 'put');
//if(!err && count[id] !== lot.s){ console.log(err = "Disk count not same as ram count."); console.STAT && console.STAT(+new Date, lot.s - count[id], 'put ack != count') } delete count[id];
if(err){ root.on('in', {'@': id, err: err, DBG: DBG}); return }
root.on('in', {'@': id, ok: ok, DBG: DBG});
//}, id, DBG && (DBG.r = DBG.r || {}));
}, false && id, DBG && (DBG.r = DBG.r || {}));
DBG && (DBG.sps = DBG.sps || +new Date);
});
var count = {}, obj_empty = Object.empty;
root.on('get', function(msg){
this.to.next(msg);
var ctx = msg._||'', DBG = ctx.DBG = msg.DBG; DBG && (DBG.sg = +new Date);
var id = msg['#'], get = msg.get, soul = msg.get['#'], has = msg.get['.']||'', o = {}, graph, lex, key, tmp, force;
if('string' == typeof soul){
key = soul;
} else
if(soul){
if(u !== (tmp = soul['*'])){ o.limit = force = 1 }
if(u !== soul['>']){ o.start = soul['>'] }
if(u !== soul['<']){ o.end = soul['<'] }
key = force? (''+tmp) : tmp || soul['='];
force = null;
}
if(key && !o.limit){ // a soul.has must be on a soul, and not during soul*
if('string' == typeof has){
key = key+esc+(o.atom = has);
} else
if(has){
if(u !== has['>']){ o.start = has['>']; o.limit = 1 }
if(u !== has['<']){ o.end = has['<']; o.limit = 1 }
if(u !== (tmp = has['*'])){ o.limit = force = 1 }
if(key){ key = key+esc + (force? (''+(tmp||'')) : tmp || (o.atom = has['='] || '')) }
}
}
if((tmp = get['%']) || o.limit){
o.limit = (tmp <= (o.pack || (1000 * 100)))? tmp : 1;
}
if(has['-'] || (soul||{})['-'] || get['-']){ o.reverse = true }
if((tmp = (root.next||'')[soul]) && tmp.put){
if(o.atom){
tmp = (tmp.next||'')[o.atom] ;
if(tmp && tmp.root && tmp.root.graph && tmp.root.graph[soul] && tmp.root.graph[soul][o.atom]){ return }
} else
if(tmp && tmp.rad){ return }
}
var now = Gun.state();
var S = (+new Date), C = 0, SPT = 0; // STATS!
DBG && (DBG.sgm = S);
//var GID = String.random(3); console.log("GET ------->>>", GID, key, o, '?', get);
dare(key||'', function(err, data, info){
//console.log("<<<------- GOT", GID, +new Date - S, err, data);
DBG && (DBG.sgr = +new Date);
DBG && (DBG.sgi = info);
try{opt.store.stats.get.time[statg % 50] = (+new Date) - S; ++statg;
opt.store.stats.get.count++;
if(err){ opt.store.stats.get.err = err }
}catch(e){} // STATS!
//if(u === data && info.chunks > 1){ return } // if we already sent a chunk, ignore ending empty responses. // this causes tests to fail.
console.STAT && console.STAT(S, +new Date - S, 'got', JSON.stringify(key)); S = +new Date;
info = info || '';
var va, ve;
if(info.unit && data && u !== (va = data[':']) && u !== (ve = data['>'])){ // new format
var tmp = key.split(esc), so = tmp[0], ha = tmp[1];
(graph = graph || {})[so] = Gun.state.ify(graph[so], ha, ve, va, so);
root.$.get(so).get(ha)._.rad = now;
// REMEMBER TO ADD _rad TO NODE/SOUL QUERY!
} else
if(data){ // old code path
if(typeof data !== 'string'){
if(o.atom){
data = u;
} else {
Radix.map(data, each, o); // IS A RADIX TREE, NOT FUNCTION!
}
}
if(!graph && data){ each(data, '') }
// TODO: !has what about soul lookups?
if(!o.atom && !has & 'string' == typeof soul && !o.limit && !o.more){
root.$.get(soul)._.rad = now;
}
}
DBG && (DBG.sgp = +new Date);
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
// Or benchmark by reusing first start date.
if(console.STAT && (ST = +new Date - S) > 9){ console.STAT(S, ST, 'got prep time'); console.STAT(S, C, 'got prep #') } SPT += ST; C = 0; S = +new Date;
var faith = function(){}; faith.faith = true; faith.rad = get; // HNPERF: We're testing performance improvement by skipping going through security again, but this should be audited.
root.on('in', {'@': id, put: graph, '%': info.more? 1 : u, err: err? err : u, _: faith, DBG: DBG});
console.STAT && (ST = +new Date - S) > 9 && console.STAT(S, ST, 'got emit', Object.keys(graph||{}).length);
graph = u; // each is outside our scope, we have to reset graph to nothing!
}, o, DBG && (DBG.r = DBG.r || {}));
DBG && (DBG.sgd = +new Date);
console.STAT && (ST = +new Date - S) > 9 && console.STAT(S, ST, 'get call'); // TODO: Perf: this was half a second??????
function each(val, has, a,b){ // TODO: THIS CODE NEEDS TO BE FASTER!!!!
C++;
if(!val){ return }
has = (key+has).split(esc);
var soul = has.slice(0,1)[0];
has = has.slice(-1)[0];
if(o.limit && o.limit <= o.count){ return true }
var va, ve, so = soul, ha = has;
//if(u !== (va = val[':']) && u !== (ve = val['>'])){ // THIS HANDLES NEW CODE!
if('string' != typeof val){ // THIS HANDLES NEW CODE!
va = val[':']; ve = val['>'];
(graph = graph || {})[so] = Gun.state.ify(graph[so], ha, ve, va, so);
//root.$.get(so).get(ha)._.rad = now;
o.count = (o.count || 0) + ((va||'').length || 9);
return;
}
o.count = (o.count || 0) + val.length;
var tmp = val.lastIndexOf('>');
var state = Radisk.decode(val.slice(tmp+1), null, esc);
val = Radisk.decode(val.slice(0,tmp), null, esc);
(graph = graph || {})[soul] = Gun.state.ify(graph[soul], has, state, val, soul);
}
});
var val_is = Gun.valid;
(opt.store||{}).stats = {get:{time:{}, count:0}, put: {time:{}, count:0}}; // STATS!
var statg = 0, statp = 0; // STATS!
});

View File

@@ -1,58 +0,0 @@
(function (env) {
var Gun;
if(typeof module !== "undefined" && module.exports){ Gun = require('gun/gun') }
if(typeof window !== "undefined"){ Gun = window.Gun }
Gun.chain.sync = function (obj, opt, cb, o) {
var gun = this;
if (!Gun.obj.is(obj)) {
console.log('First param is not an object');
return gun;
}
if (Gun.bi.is(opt)) {
opt = {
meta: opt
};
}
if(Gun.fn.is(opt)){
cb = opt;
opt = null;
}
cb = cb || function(){};
opt = opt || {};
opt.ctx = opt.ctx || {};
gun.on(function (change, field) {
Gun.obj.map(change, function (val, field) {
if (!obj) {
return;
}
if (field === '_' || field === '#') {
if (opt.meta) {
obj[field] = val;
}
return;
}
if (Gun.obj.is(val)) {
var soul = Gun.val.rel.is(val);
if (opt.ctx[soul + field]) {
// don't re-subscribe.
return;
}
// unique subscribe!
opt.ctx[soul + field] = true;
this.path(field).sync(
obj[field] = (obj[field] || {}),
Gun.obj.copy(opt),
cb,
o || obj
);
return;
}
obj[field] = val;
}, this);
cb(o || obj);
});
return gun;
};
}());

View File

@@ -1,134 +0,0 @@
;(function(){
var GUN = (typeof window !== "undefined")? window.Gun : require('../gun');
GUN.on('opt', function(root){
this.to.next(root);
var opt = root.opt;
if(root.once){ return }
if(!GUN.Mesh){ return }
if(false === opt.RTCPeerConnection){ return }
var env;
if(typeof window !== "undefined"){ env = window }
if(typeof global !== "undefined"){ env = global }
env = env || {};
var rtcpc = opt.RTCPeerConnection || env.RTCPeerConnection || env.webkitRTCPeerConnection || env.mozRTCPeerConnection;
var rtcsd = opt.RTCSessionDescription || env.RTCSessionDescription || env.webkitRTCSessionDescription || env.mozRTCSessionDescription;
var rtcic = opt.RTCIceCandidate || env.RTCIceCandidate || env.webkitRTCIceCandidate || env.mozRTCIceCandidate;
if(!rtcpc || !rtcsd || !rtcic){ return }
opt.RTCPeerConnection = rtcpc;
opt.RTCSessionDescription = rtcsd;
opt.RTCIceCandidate = rtcic;
opt.rtc = opt.rtc || {'iceServers': [
{urls: 'stun:stun.l.google.com:19302'},
{urls: 'stun:stun.cloudflare.com:3478'}/*,
{urls: "stun:stun.sipgate.net:3478"},
{urls: "stun:stun.stunprotocol.org"},
{urls: "stun:stun.sipgate.net:10000"},
{urls: "stun:217.10.68.152:10000"},
{urls: 'stun:stun.services.mozilla.com'}*/
]};
// TODO: Select the most appropriate stuns.
// FIXME: Find the wire throwing ICE Failed
// The above change corrects at least firefox RTC Peer handler where it **throws** on over 6 ice servers, and updates url: to urls: removing deprecation warning
opt.rtc.dataChannel = opt.rtc.dataChannel || {ordered: false, maxRetransmits: 2};
opt.rtc.sdp = opt.rtc.sdp || {mandatory: {OfferToReceiveAudio: false, OfferToReceiveVideo: false}};
opt.rtc.max = opt.rtc.max || 55; // is this a magic number? // For Future WebRTC notes: Chrome 500 max limit, however 256 likely - FF "none", webtorrent does 55 per torrent.
opt.rtc.room = opt.rtc.room || GUN.window && (window.rtcRoom || location.hash.slice(1) || location.pathname.slice(1));
opt.announce = function(to){
opt.rtc.start = +new Date; // handle room logic:
root.$.get('/RTC/'+opt.rtc.room+'<?99').get('+').put(opt.pid, function(ack){
if(!ack.ok || !ack.ok.rtc){ return }
plan(ack);
}, {acks: opt.rtc.max}).on(function(last,key, msg){
if(last === opt.pid || opt.rtc.start > msg.put['>']){ return }
plan({'#': ''+msg['#'], ok: {rtc: {id: last}}});
});
};
var mesh = opt.mesh = opt.mesh || GUN.Mesh(root), wired = mesh.wire;
mesh.hear['rtc'] = plan;
mesh.wire = function(media){ try{ wired && wired(media);
if(!(media instanceof MediaStream)){ return }
(open.media = open.media||{})[media.id] = media;
for(var p in opt.peers){ p = opt.peers[p]||'';
p.addTrack && media.getTracks().forEach(track => {
p.addTrack(track, media);
});
p.createOffer && p.createOffer(function(offer){
p.setLocalDescription(offer);
mesh.say({'#': root.ask(plan), dam: 'rtc', ok: {rtc: {offer: offer, id: opt.pid}}}, p);
}, function(){}, opt.rtc.sdp);
}
} catch(e){console.log(e)} }
root.on('create', function(at){
this.to.next(at);
setTimeout(opt.announce, 1);
});
function plan(msg){
if(!msg.ok){ return }
var rtc = msg.ok.rtc, peer, tmp;
if(!rtc || !rtc.id || rtc.id === opt.pid){ return }
peer = open(msg, rtc);
if(tmp = rtc.candidate){
return peer.addIceCandidate(new opt.RTCIceCandidate(tmp));
}
if(tmp = rtc.answer){
tmp.sdp = tmp.sdp.replace(/\\r\\n/g, '\r\n');
return peer.setRemoteDescription(peer.remoteSet = new opt.RTCSessionDescription(tmp));
}
if(tmp = rtc.offer){
rtc.offer.sdp = rtc.offer.sdp.replace(/\\r\\n/g, '\r\n');
peer.setRemoteDescription(new opt.RTCSessionDescription(tmp));
return peer.createAnswer(function(answer){
peer.setLocalDescription(answer);
root.on('out', {'@': msg['#'], ok: {rtc: {answer: answer, id: opt.pid}}});
}, function(){}, opt.rtc.sdp);
}
}
function open(msg, rtc, peer){
if(peer = opt.peers[rtc.id] || open[rtc.id]){ return peer }
(peer = new opt.RTCPeerConnection(opt.rtc)).id = rtc.id;
var wire = peer.wire = peer.createDataChannel('dc', opt.rtc.dataChannel);
function rtceve(eve){ eve.peer = peer; gun.on('rtc', eve) }
peer.$ = gun;
open[rtc.id] = peer;
peer.ontrack = rtceve;
peer.onremovetrack = rtceve;
peer.onconnectionstatechange = rtceve;
wire.to = setTimeout(function(){delete open[rtc.id]},1000*60);
wire.onclose = function(){ mesh.bye(peer) };
wire.onerror = function(err){ };
wire.onopen = function(e){
delete open[rtc.id];
mesh.hi(peer);
}
wire.onmessage = function(msg){
if(!msg){ return }
mesh.hear(msg.data || msg, peer);
};
peer.onicecandidate = function(e){ rtceve(e);
if(!e.candidate){ return }
root.on('out', {'@': (msg||'')['#'], '#': root.ask(plan), ok: {rtc: {candidate: e.candidate, id: opt.pid}}});
}
peer.ondatachannel = function(e){ rtceve(e);
var rc = e.channel;
rc.onmessage = wire.onmessage;
rc.onopen = wire.onopen;
rc.onclose = wire.onclose;
}
if(rtc.offer){ return peer }
for(var m in open.media){ m = open.media[m];
m.getTracks().forEach(track => {
peer.addTrack(track, m);
});
}
peer.createOffer(function(offer){
peer.setLocalDescription(offer);
root.on('out', {'@': (msg||'')['#'], '#': root.ask(plan), ok: {rtc: {offer: offer, id: opt.pid}}});
}, function(){}, opt.rtc.sdp);
return peer;
}
});
}());

View File

@@ -1,244 +0,0 @@
;(function(){
// JSON: JavaScript Object Notation
// YSON: Yielding javaScript Object Notation
var yson = {}, u, sI = setTimeout.turn || (typeof setImmediate != ''+u && setImmediate) || setTimeout;
yson.parseAsync = function(text, done, revive, M){
if('string' != typeof text){ try{ done(u,JSON.parse(text)) }catch(e){ done(e) } return }
var ctx = {i: 0, text: text, done: done, l: text.length, up: []};
//M = 1024 * 1024 * 100;
//M = M || 1024 * 64;
M = M || 1024 * 32;
parse();
function parse(){
//var S = +new Date;
var s = ctx.text;
var i = ctx.i, l = ctx.l, j = 0;
var w = ctx.w, b, tmp;
while(j++ < M){
var c = s[i++];
if(i > l){
ctx.end = true;
break;
}
if(w){
i = s.indexOf('"', i-1); c = s[i];
tmp = 0; while('\\' == s[i-(++tmp)]){}; tmp = !(tmp % 2);//tmp = ('\\' == s[i-1]); // json is stupid
b = b || tmp;
if('"' == c && !tmp){
w = u;
tmp = ctx.s;
if(ctx.a){
tmp = s.slice(ctx.sl, i);
if(b || (1+tmp.indexOf('\\'))){ tmp = JSON.parse('"'+tmp+'"') } // escape + unicode :( handling
if(ctx.at instanceof Array){
ctx.at.push(ctx.s = tmp);
} else {
if(!ctx.at){ ctx.end = j = M; tmp = u }
(ctx.at||{})[ctx.s] = ctx.s = tmp;
}
ctx.s = u;
} else {
ctx.s = s.slice(ctx.sl, i);
if(b || (1+ctx.s.indexOf('\\'))){ ctx.s = JSON.parse('"'+ctx.s+'"'); } // escape + unicode :( handling
}
ctx.a = b = u;
}
++i;
} else {
switch(c){
case '"':
ctx.sl = i;
w = true;
break;
case ':':
ctx.ai = i;
ctx.a = true;
break;
case ',':
if(ctx.a || ctx.at instanceof Array){
if(tmp = s.slice(ctx.ai, i-1)){
if(u !== (tmp = value(tmp))){
if(ctx.at instanceof Array){
ctx.at.push(tmp);
} else {
ctx.at[ctx.s] = tmp;
}
}
}
}
ctx.a = u;
if(ctx.at instanceof Array){
ctx.a = true;
ctx.ai = i;
}
break;
case '{':
ctx.up.push(ctx.at||(ctx.at = {}));
if(ctx.at instanceof Array){
ctx.at.push(ctx.at = {});
} else
if(u !== (tmp = ctx.s)){
ctx.at[tmp] = ctx.at = {};
}
ctx.a = u;
break;
case '}':
if(ctx.a){
if(tmp = s.slice(ctx.ai, i-1)){
if(u !== (tmp = value(tmp))){
if(ctx.at instanceof Array){
ctx.at.push(tmp);
} else {
if(!ctx.at){ ctx.end = j = M; tmp = u }
(ctx.at||{})[ctx.s] = tmp;
}
}
}
}
ctx.a = u;
ctx.at = ctx.up.pop();
break;
case '[':
if(u !== (tmp = ctx.s)){
ctx.up.push(ctx.at);
ctx.at[tmp] = ctx.at = [];
} else
if(!ctx.at){
ctx.up.push(ctx.at = []);
}
ctx.a = true;
ctx.ai = i;
break;
case ']':
if(ctx.a){
if(tmp = s.slice(ctx.ai, i-1)){
if(u !== (tmp = value(tmp))){
if(ctx.at instanceof Array){
ctx.at.push(tmp);
} else {
ctx.at[ctx.s] = tmp;
}
}
}
}
ctx.a = u;
ctx.at = ctx.up.pop();
break;
}
}
}
ctx.s = u;
ctx.i = i;
ctx.w = w;
if(ctx.end){
tmp = ctx.at;
if(u === tmp){
try{ tmp = JSON.parse(text)
}catch(e){ return ctx.done(e) }
}
ctx.done(u, tmp);
} else {
sI(parse);
}
}
}
function value(s){
var n = parseFloat(s);
if(!isNaN(n)){
return n;
}
s = s.trim();
if('true' == s){
return true;
}
if('false' == s){
return false;
}
if('null' == s){
return null;
}
}
yson.stringifyAsync = function(data, done, replacer, space, ctx){
//try{done(u, JSON.stringify(data, replacer, space))}catch(e){done(e)}return;
ctx = ctx || {};
ctx.text = ctx.text || "";
ctx.up = [ctx.at = {d: data}];
ctx.done = done;
ctx.i = 0;
var j = 0;
ify();
function ify(){
var at = ctx.at, data = at.d, add = '', tmp;
if(at.i && (at.i - at.j) > 0){ add += ',' }
if(u !== (tmp = at.k)){ add += JSON.stringify(tmp) + ':' } //'"'+tmp+'":' } // only if backslash
switch(typeof data){
case 'boolean':
add += ''+data;
break;
case 'string':
add += JSON.stringify(data); //ctx.text += '"'+data+'"';//JSON.stringify(data); // only if backslash
break;
case 'number':
add += (isNaN(data)? 'null' : data);
break;
case 'object':
if(!data){
add += 'null';
break;
}
if(data instanceof Array){
add += '[';
at = {i: -1, as: data, up: at, j: 0};
at.l = data.length;
ctx.up.push(ctx.at = at);
break;
}
if('function' != typeof (data||'').toJSON){
add += '{';
at = {i: -1, ok: Object.keys(data).sort(), as: data, up: at, j: 0};
at.l = at.ok.length;
ctx.up.push(ctx.at = at);
break;
}
if(tmp = data.toJSON()){
add += tmp;
break;
}
// let this & below pass into default case...
case 'function':
if(at.as instanceof Array){
add += 'null';
break;
}
default: // handle wrongly added leading `,` if previous item not JSON-able.
add = '';
at.j++;
}
ctx.text += add;
while(1+at.i >= at.l){
ctx.text += (at.ok? '}' : ']');
at = ctx.at = at.up;
}
if(++at.i < at.l){
if(tmp = at.ok){
at.d = at.as[at.k = tmp[at.i]];
} else {
at.d = at.as[at.i];
}
if(++j < 9){ return ify() } else { j = 0 }
sI(ify);
return;
}
ctx.done(u, ctx.text);
}
}
if(typeof window != ''+u){ window.YSON = yson }
try{ if(typeof module != ''+u){ module.exports = yson } }catch(e){}
if(typeof JSON != ''+u){
JSON.parseAsync = yson.parseAsync;
JSON.stringifyAsync = yson.stringifyAsync;
}
}());

View File

@@ -14,7 +14,7 @@ def get_all_files(directory):
return files
PREFETCH = ""
VERSIONCO = "2025-08"
VERSIONCO = "2026-02"
HANDLEPARSE = get_all_files("src")
TITLE = os.environ.get("TELESEC_TITLE", "TeleSec")
HOSTER = os.environ.get("TELESEC_HOSTER", "EuskadiTech")
@@ -35,7 +35,7 @@ shutil.copytree("assets","dist", dirs_exist_ok=True)
def replace_handles(string):
string = string.replace("%%PREFETCH%%", PREFETCH)
string = string.replace("%%VERSIONCO%%", VERSIONCO)
string = string.replace("%%TITLE%%", "TeleSec")
string = string.replace("%%TITLE%%", TITLE)
string = string.replace("%%HOSTER%%", HOSTER)
string = string.replace("%%ASSETSJSON%%", json.dumps(ASSETS, ensure_ascii=False))
return string

View File

@@ -1,33 +1,33 @@
function fixfloat(number) {
return (parseFloat(number).toPrecision(8));
return parseFloat(number).toPrecision(8);
}
function tableScroll(query) {
$(query).doubleScroll();
}
//var secretTokenEl = document.getElementById("secretToken");
var container = document.getElementById("container");
var container = document.getElementById('container');
function open_page(params) {
// Clear stored event listeners and timers
EventListeners.GunJS = [];
EventListeners.Timeout.forEach(ev => clearTimeout(ev));
EventListeners.Timeout.forEach((ev) => clearTimeout(ev));
EventListeners.Timeout = [];
EventListeners.Interval.forEach(ev => clearInterval(ev));
EventListeners.Interval.forEach((ev) => clearInterval(ev));
EventListeners.Interval = [];
EventListeners.QRScanner.forEach(ev => ev.clear());
EventListeners.QRScanner.forEach((ev) => ev.clear());
EventListeners.QRScanner = [];
EventListeners.Custom.forEach(ev => ev());
EventListeners.Custom.forEach((ev) => ev());
EventListeners.Custom = [];
if (SUB_LOGGED_IN != true && params != "login,setup" && !params.startsWith("login,onboarding")) {
PAGES["login"].index();
if (SUB_LOGGED_IN != true && params != 'login,setup' && !params.startsWith('login,onboarding')) {
PAGES['login'].index();
return;
}
if (params == "") {
params = "index";
if (params == '') {
params = 'index';
}
var path = params.split(",");
var path = params.split(',');
var app = path[0];
if (path[1] == undefined) {
PAGES[app].index();
@@ -37,8 +37,8 @@ function open_page(params) {
}
function setUrlHash(hash) {
location.hash = "#" + hash;
location.hash = '#' + hash;
// Handle quick search transfer
if (hash === 'buscar') {
const quickSearchInput = document.getElementById('quickSearchInput');
@@ -50,62 +50,57 @@ function setUrlHash(hash) {
}
}
window.onhashchange = () => {
open_page(location.hash.replace("#", ""));
open_page(location.hash.replace('#', ''));
};
function download(filename, text) {
var element = document.createElement("a");
var element = document.createElement('a');
element.setAttribute(
"href",
"data:application/octet-stream;charset=utf-8," + encodeURIComponent(text)
'href',
'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text)
);
element.setAttribute("download", filename);
element.style.display = "none";
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function resizeInputImage(
file,
callback,
targetHeight = 256,
targetQuality = 0.75
) {
function resizeInputImage(file, callback, targetHeight = 256, targetQuality = 0.75) {
const reader = new FileReader();
reader.onload = function(event) {
reader.onload = function (event) {
const img = new Image();
img.onload = function() {
img.onload = function () {
const aspectRatio = img.width / img.height;
const targetWidth = targetHeight * aspectRatio;
const canvas = document.createElement("canvas");
const canvas = document.createElement('canvas');
canvas.width = targetWidth;
canvas.height = targetHeight;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ffffff";
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
// Get resized image as Blob
const dataURL = canvas.toDataURL("image/jpeg", targetQuality);
const dataURL = canvas.toDataURL('image/jpeg', targetQuality);
callback(dataURL);
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
}
function CurrentISODate() {
return new Date().toISOString().split("T")[0].replace("T", " ");
return new Date().toISOString().split('T')[0].replace('T', ' ');
}
function CurrentISOTime() {
@@ -113,7 +108,7 @@ function CurrentISOTime() {
}
function fixGunLocalStorage() {
localStorage.removeItem("radata");
localStorage.removeItem('radata');
removeCache();
location.reload();
}
@@ -125,45 +120,34 @@ function fixGunLocalStorage() {
// }
// }, 5000);
function betterSorter(a, b) {
// 1. Fecha (ascending)
if (a.Fecha && b.Fecha && a.Fecha !== b.Fecha) {
return a.Fecha > b.Fecha ? -1 : 1;
}
// 2. Region (ascending, from SC_Personas if Persona exists)
const regionA =
a.Persona && SC_Personas[a.Persona]
? SC_Personas[a.Persona].Region || ""
: a.Region || "";
const regionB =
b.Persona && SC_Personas[b.Persona]
? SC_Personas[b.Persona].Region || ""
: b.Region || "";
if (regionA !== regionB) {
return regionA.toLowerCase() < regionB.toLowerCase() ? -1 : 1;
}
// 3. Persona (Nombre, ascending, from SC_Personas if Persona exists)
const nombrePersonaA =
a.Persona && SC_Personas[a.Persona]
? SC_Personas[a.Persona].Nombre || ""
: "";
const nombrePersonaB =
b.Persona && SC_Personas[b.Persona]
? SC_Personas[b.Persona].Nombre || ""
: "";
if (nombrePersonaA !== nombrePersonaB) {
return nombrePersonaA.toLowerCase() < nombrePersonaB.toLowerCase()
? -1
: 1;
}
// 4. Nombre (ascending, from a.Nombre/b.Nombre)
if (a.Nombre && b.Nombre && a.Nombre !== b.Nombre) {
return a.Nombre.toLowerCase() < b.Nombre.toLowerCase() ? -1 : 1;
}
// 5. Asunto (ascending, from a.Asunto/b.Asunto)
if (a.Asunto && b.Asunto && a.Asunto !== b.Asunto) {
return a.Asunto.toLowerCase() < b.Asunto.toLowerCase() ? -1 : 1;
}
return 0;
// 1. Fecha (ascending)
if (a.Fecha && b.Fecha && a.Fecha !== b.Fecha) {
return a.Fecha > b.Fecha ? -1 : 1;
}
// 2. Region (ascending, from SC_Personas if Persona exists)
const regionA =
a.Persona && SC_Personas[a.Persona] ? SC_Personas[a.Persona].Region || '' : a.Region || '';
const regionB =
b.Persona && SC_Personas[b.Persona] ? SC_Personas[b.Persona].Region || '' : b.Region || '';
if (regionA !== regionB) {
return regionA.toLowerCase() < regionB.toLowerCase() ? -1 : 1;
}
// 3. Persona (Nombre, ascending, from SC_Personas if Persona exists)
const nombrePersonaA =
a.Persona && SC_Personas[a.Persona] ? SC_Personas[a.Persona].Nombre || '' : '';
const nombrePersonaB =
b.Persona && SC_Personas[b.Persona] ? SC_Personas[b.Persona].Nombre || '' : '';
if (nombrePersonaA !== nombrePersonaB) {
return nombrePersonaA.toLowerCase() < nombrePersonaB.toLowerCase() ? -1 : 1;
}
// 4. Nombre (ascending, from a.Nombre/b.Nombre)
if (a.Nombre && b.Nombre && a.Nombre !== b.Nombre) {
return a.Nombre.toLowerCase() < b.Nombre.toLowerCase() ? -1 : 1;
}
// 5. Asunto (ascending, from a.Asunto/b.Asunto)
if (a.Asunto && b.Asunto && a.Asunto !== b.Asunto) {
return a.Asunto.toLowerCase() < b.Asunto.toLowerCase() ? -1 : 1;
}
return 0;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,7 @@
// Syntax helper for HTML template literals (e.g. html`<div>${content}</div>`)
const html = (strings, ...values) => String.raw({ raw: strings }, ...values);
// Global Event Listeners registry for cleanup on logout or other events. Each category can be used to track different types of listeners (e.g., GunJS events, timeouts, intervals, QRScanner events, custom events).
var EventListeners = {
GunJS: [],
Timeout: [],
@@ -6,60 +10,120 @@ var EventListeners = {
Custom: [],
};
function safeuuid(prefix = "AXLUID_") {
// Safe UUID for html element IDs: generates a unique identifier with a specified prefix, ensuring it is safe for use in HTML element IDs. It uses crypto.randomUUID if available, with a fallback to a random string generation method for environments that do not support it. The generated ID is prefixed to avoid collisions and ensure uniqueness across the application.
function safeuuid(prefix = 'AXLUID_') {
if (!crypto.randomUUID) {
// Fallback for environments without crypto.randomUUID()
const randomPart = Math.random().toString(36).substring(2, 10);
return prefix + randomPart;
}
return prefix + crypto.randomUUID().split("-")[4];
return prefix + crypto.randomUUID().split('-')[4];
}
function parseURL(input) {
try {
return new URL(input);
} catch (e) {
try {
return new URL('https://' + input);
} catch (e2) {
return { hostname: '', username: '', password: '', pathname: '' };
}
}
}
var urlParams = new URLSearchParams(location.search);
var AC_BYPASS = false;
if (urlParams.get("ac_bypass") == "yes") {
if (urlParams.get('ac_bypass') == 'yes') {
AC_BYPASS = true;
}
if (urlParams.get("hidenav") != undefined) {
document.getElementById("header_hide_query").style.display = "none";
if (urlParams.get('hidenav') != undefined) {
document.getElementById('header_hide_query').style.display = 'none';
}
// CouchDB URI generator from components: host, user, pass, dbname. Host can include protocol or not, but will be normalized to just hostname in the display. If host is empty, returns empty string.
function makeCouchURLDisplay(host, user, pass, dbname) {
if (!host) return '';
var display = user + ':' + pass + '@' + host.replace(/^https?:\/\//, '') + '/' + dbname;
return display;
}
// Auto-configure CouchDB from ?couch=<uri> parameter
if (urlParams.get('couch') != null) {
try {
var couchURI = urlParams.get('couch');
// Normalize URL: add https:// if no protocol specified
var normalizedUrl = couchURI;
if (!/^https?:\/\//i.test(couchURI)) {
normalizedUrl = 'https://' + couchURI;
}
var URL_PARSED = parseURL(normalizedUrl);
var user = URL_PARSED.username || '';
var pass = URL_PARSED.password || '';
var dbname = URL_PARSED.pathname ? URL_PARSED.pathname.replace(/^\//, '') : '';
var host = URL_PARSED.hostname || normalizedUrl;
// Extract secret from ?secret= parameter if provided
var secret = urlParams.get('secret') || '';
// Save to localStorage
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + host);
localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
localStorage.setItem('TELESEC_COUCH_USER', user);
localStorage.setItem('TELESEC_COUCH_PASS', pass);
if (secret) {
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
}
// Mark onboarding as complete since we have server config
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
// Clean URL by removing the couch parameter
urlParams.delete('couch');
urlParams.delete('secret');
history.replaceState(
null,
'',
location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '') + location.hash
);
console.log('CouchDB auto-configured from URL parameter');
} catch (e) {
console.error('Error auto-configuring CouchDB from URL:', e);
}
}
// getDBName: prefer explicit CouchDB dbname from settings. Single-group model: default to 'telesec'
function getDBName() {
const dbname = localStorage.getItem('TELESEC_COUCH_DBNAME') || '';
if (dbname && dbname.trim() !== '') return dbname.trim();
return 'telesec';
}
// const PUBLIC_KEY = "~cppGiuA4UFUPGTDoC-4r2izVC3F7MfpaCmF3iZdESN4.vntmjgbAVUpF_zfinYY6EKVFuuTYxh5xOrL4KmtdTmc"
// `TABLE` variable removed. The CouchDB database name should be configured via the login/setup form
// and passed to `DB.init({ dbname: '<your-db>' })` so it becomes the app's primary DB.
// Legacy relay list removed (migrated to CouchDB/PouchDB)
const RELAYS = [];
var SECRET = "";
var SECRET = '';
var SUB_LOGGED_IN = false;
var SUB_LOGGED_IN_DETAILS = false;
var SUB_LOGGED_IN_ID = false;
var SAVE_WAIT = 500;
var SC_Personas = {};
var PeerConnectionInterval = 5000;
if (urlParams.get("sublogin") != null) {
if (urlParams.get('sublogin') != null) {
SUB_LOGGED_IN = true;
SUB_LOGGED_IN_ID = urlParams.get("sublogin");
SUB_LOGGED_IN_ID = urlParams.get('sublogin');
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID];
var sli = 15;
var slii = setInterval(() => {
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID];
sli-=1;
sli -= 1;
if (sli < 0) {
clearInterval(slii);
}
}, 500);
}
// Logout function for sublogin: clears sublogin state and reloads the page without the sublogin parameter
function LogOutTeleSec() {
SUB_LOGGED_IN = false;
SUB_LOGGED_IN_DETAILS = false;
SUB_LOGGED_IN_ID = false;
document.getElementById("loading").style.display = "block";
document.getElementById('loading').style.display = 'block';
//Remove sublogin from URL and reload
urlParams.delete("sublogin");
history.replaceState(null, "", "?" + urlParams.toString());
urlParams.delete('sublogin');
history.replaceState(null, '', '?' + urlParams.toString());
location.reload();
}

166
src/db.js
View File

@@ -18,9 +18,13 @@ var DB = (function () {
const localName = 'telesec';
local = new PouchDB(localName);
if (changes) {
try { changes.cancel(); } catch (e) {}
try {
changes.cancel();
} catch (e) {}
}
changes = local.changes({ live: true, since: 'now', include_docs: true }).on('change', onChange);
changes = local
.changes({ live: true, since: 'now', include_docs: true })
.on('change', onChange);
} catch (e) {
console.warn('ensureLocal error', e);
}
@@ -35,7 +39,9 @@ var DB = (function () {
try {
if (opts && opts.secret) {
SECRET = opts.secret;
try { localStorage.setItem('TELESEC_SECRET', SECRET); } catch (e) {}
try {
localStorage.setItem('TELESEC_SECRET', SECRET);
} catch (e) {}
}
} catch (e) {}
local = new PouchDB(localName);
@@ -43,7 +49,7 @@ var DB = (function () {
if (opts.remoteServer) {
try {
const server = opts.remoteServer.replace(/\/$/, '');
const dbname = encodeURIComponent((opts.dbname || localName));
const dbname = encodeURIComponent(opts.dbname || localName);
let authPart = '';
if (opts.username) authPart = opts.username + ':' + (opts.password || '') + '@';
const remoteUrl = server.replace(/https?:\/\//, (m) => m) + '/' + dbname;
@@ -56,25 +62,41 @@ var DB = (function () {
}
if (changes) changes.cancel();
changes = local.changes({ live: true, since: 'now', include_docs: true }).on('change', onChange);
changes = local
.changes({ live: true, since: 'now', include_docs: true })
.on('change', onChange);
return Promise.resolve();
}
function replicateToRemote() {
ensureLocal();
if (!local || !remote) return;
try { if (repPush && repPush.cancel) repPush.cancel(); } catch (e) {}
try { if (repPull && repPull.cancel) repPull.cancel(); } catch (e) {}
try {
if (repPush && repPush.cancel) repPush.cancel();
} catch (e) {}
try {
if (repPull && repPull.cancel) repPull.cancel();
} catch (e) {}
repPush = PouchDB.replicate(local, remote, { live: true, retry: true })
.on('error', function (err) { console.warn('Replication push error', err); });
repPull = PouchDB.replicate(remote, local, { live: true, retry: true })
.on('error', function (err) { console.warn('Replication pull error', err); });
repPush = PouchDB.replicate(local, remote, { live: true, retry: true }).on(
'error',
function (err) {
console.warn('Replication push error', err);
}
);
repPull = PouchDB.replicate(remote, local, { live: true, retry: true }).on(
'error',
function (err) {
console.warn('Replication pull error', err);
}
);
}
if (typeof window !== 'undefined' && window.addEventListener) {
window.addEventListener('online', function () {
try { setTimeout(replicateToRemote, 1000); } catch (e) {}
try {
setTimeout(replicateToRemote, 1000);
} catch (e) {}
});
}
@@ -86,7 +108,7 @@ var DB = (function () {
// derive a stable color from the last record's data hash
let payload = '';
try {
payload = (typeof doc.data === 'string') ? doc.data : JSON.stringify(doc.data || {});
payload = typeof doc.data === 'string' ? doc.data : JSON.stringify(doc.data || {});
} catch (e) {
payload = String(doc._id || '');
}
@@ -104,8 +126,12 @@ var DB = (function () {
if (change.deleted || doc._deleted) {
delete docCache[doc._id];
if (callbacks[table]) {
callbacks[table].forEach(cb => {
try { cb(null, id); } catch (e) { console.error(e); }
callbacks[table].forEach((cb) => {
try {
cb(null, id);
} catch (e) {
console.error(e);
}
});
}
return;
@@ -117,11 +143,17 @@ var DB = (function () {
const prev = docCache[doc._id];
if (prev === now) return; // no meaningful change
docCache[doc._id] = now;
} catch (e) { /* ignore cache errors */ }
} catch (e) {
/* ignore cache errors */
}
if (callbacks[table]) {
callbacks[table].forEach(cb => {
try { cb(doc.data, id); } catch (e) { console.error(e); }
callbacks[table].forEach((cb) => {
try {
cb(doc.data, id);
} catch (e) {
console.error(e);
}
});
}
}
@@ -138,13 +170,25 @@ var DB = (function () {
const doc = existing || { _id: _id };
var toStore = data;
try {
var isEncryptedString = (typeof data === 'string' && data.startsWith('RSA{') && data.endsWith('}'));
if (!isEncryptedString && typeof TS_encrypt === 'function' && typeof SECRET !== 'undefined' && SECRET) {
toStore = await new Promise(resolve => {
try { TS_encrypt(data, SECRET, enc => resolve(enc)); } catch (e) { resolve(data); }
var isEncryptedString =
typeof data === 'string' && data.startsWith('RSA{') && data.endsWith('}');
if (
!isEncryptedString &&
typeof TS_encrypt === 'function' &&
typeof SECRET !== 'undefined' &&
SECRET
) {
toStore = await new Promise((resolve) => {
try {
TS_encrypt(data, SECRET, (enc) => resolve(enc));
} catch (e) {
resolve(data);
}
});
}
} catch (e) { toStore = data; }
} catch (e) {
toStore = data;
}
doc.data = toStore;
doc.table = table;
doc.ts = new Date().toISOString();
@@ -154,7 +198,6 @@ var DB = (function () {
// FIX: manually trigger map() callbacks for local update
// onChange will update docCache and notify all subscribers
onChange({ doc: doc });
} catch (e) {
console.error('DB.put error', e);
}
@@ -166,21 +209,33 @@ var DB = (function () {
try {
const doc = await local.get(_id);
return doc.data;
} catch (e) { return null; }
} catch (e) {
return null;
}
}
async function del(table, id) { return put(table, id, null); }
async function del(table, id) {
return put(table, id, null);
}
async function list(table) {
ensureLocal();
try {
const res = await local.allDocs({ include_docs: true, startkey: table + ':', endkey: table + ':\uffff' });
return res.rows.map(r => {
const res = await local.allDocs({
include_docs: true,
startkey: table + ':',
endkey: table + ':\uffff',
});
return res.rows.map((r) => {
const id = r.id.split(':')[1];
try { docCache[r.id] = typeof r.doc.data === 'string' ? r.doc.data : JSON.stringify(r.doc.data); } catch (e) {}
try {
docCache[r.id] = typeof r.doc.data === 'string' ? r.doc.data : JSON.stringify(r.doc.data);
} catch (e) {}
return { id: id, data: r.doc.data };
});
} catch (e) { return []; }
} catch (e) {
return [];
}
}
function dataURLtoBlob(dataurl) {
@@ -203,11 +258,15 @@ var DB = (function () {
doc = await local.get(_id);
}
let blob = dataUrlOrBlob;
if (typeof dataUrlOrBlob === 'string' && dataUrlOrBlob.indexOf('data:') === 0) blob = dataURLtoBlob(dataUrlOrBlob);
if (typeof dataUrlOrBlob === 'string' && dataUrlOrBlob.indexOf('data:') === 0)
blob = dataURLtoBlob(dataUrlOrBlob);
const type = contentType || (blob && blob.type) || 'application/octet-stream';
await local.putAttachment(_id, name, doc._rev, blob, type);
return true;
} catch (e) { console.error('putAttachment error', e); return false; }
} catch (e) {
console.error('putAttachment error', e);
return false;
}
}
async function getAttachment(table, id, name) {
@@ -218,11 +277,13 @@ var DB = (function () {
if (!blob) return null;
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = e => resolve(e.target.result);
reader.onerror = e => reject(e);
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e);
reader.readAsDataURL(blob);
});
} catch (e) { return null; }
} catch (e) {
return null;
}
}
async function listAttachments(table, id) {
@@ -259,10 +320,14 @@ var DB = (function () {
try {
const durl = await getAttachment(table, id, name);
out.push({ name: name, dataUrl: durl, content_type: null });
} catch (e) { out.push({ name: name, dataUrl: null, content_type: null }); }
} catch (e) {
out.push({ name: name, dataUrl: null, content_type: null });
}
}
return out;
} catch (e2) { return []; }
} catch (e2) {
return [];
}
}
}
@@ -275,15 +340,20 @@ var DB = (function () {
delete doc._attachments[name];
await local.put(doc);
return true;
} catch (e) { console.error('deleteAttachment error', e); return false; }
} catch (e) {
console.error('deleteAttachment error', e);
return false;
}
}
function map(table, cb) {
ensureLocal();
callbacks[table] = callbacks[table] || [];
callbacks[table].push(cb);
list(table).then(rows => rows.forEach(r => cb(r.data, r.id)));
return () => { callbacks[table] = callbacks[table].filter(x => x !== cb); }
list(table).then((rows) => rows.forEach((r) => cb(r.data, r.id)));
return () => {
callbacks[table] = callbacks[table].filter((x) => x !== cb);
};
}
return {
@@ -298,7 +368,7 @@ var DB = (function () {
deleteAttachment,
putAttachment,
getAttachment,
_internal: { local }
_internal: { local },
};
})();
@@ -311,7 +381,15 @@ window.DB = DB;
const username = localStorage.getItem('TELESEC_COUCH_USER') || '';
const password = localStorage.getItem('TELESEC_COUCH_PASS') || '';
const dbname = localStorage.getItem('TELESEC_COUCH_DBNAME') || undefined;
try { SECRET = localStorage.getItem('TELESEC_SECRET') || ''; } catch (e) { SECRET = ''; }
DB.init({ remoteServer, username, password, dbname }).catch(e => console.warn('DB.autoInit error', e));
} catch (e) { console.warn('DB.autoInit unexpected error', e); }
try {
SECRET = localStorage.getItem('TELESEC_SECRET') || '';
} catch (e) {
SECRET = '';
}
DB.init({ remoteServer, username, password, dbname }).catch((e) =>
console.warn('DB.autoInit error', e)
);
} catch (e) {
console.warn('DB.autoInit unexpected error', e);
}
})();

View File

@@ -1,4 +1,4 @@
// gun_init.js - Deprecated
// Gun/GunDB has been replaced by PouchDB/CouchDB in this project.
// This file is kept for reference only and should not be used.
console.warn('gun_init.js is deprecated; using PouchDB/DB module instead.');
console.warn('gun_init.js is deprecated; using PouchDB/DB module instead.');

View File

@@ -68,11 +68,8 @@
<script src="static/qrcode/qrcode.min.js"></script>
<script src="static/jquery.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pouchdb@7.3.1/dist/pouchdb.min.js"></script>
<!--<script src="static/synchronous.js"></script>-->
<!--<script src="static/axe.js"></script>-->
<script src="static/toastr.min.js"></script>
<script src="static/doublescroll.js"></script>
<!--<script src="static/simplemde.min.js"></script>-->
<script src="pwa.js"></script>
<script src="config.js"></script>
<script src="db.js"></script>

View File

@@ -1,63 +1,105 @@
PERMS["aulas"] = "Aulas (Solo docentes!)";
PERMS['aulas'] = 'Aulas (Solo docentes!)';
PAGES.aulas = {
//navcss: "btn1",
Title: "Gest-Aula",
icon: "static/appico/components.png",
Title: 'Gest-Aula',
icon: 'static/appico/components.png',
AccessControl: true,
index: function () {
if (!checkRole("aulas")) {
setUrlHash("index");
if (!checkRole('aulas')) {
setUrlHash('index');
return;
}
var data_Comedor = safeuuid();
var data_Tareas = safeuuid();
var data_Diario = safeuuid();
var data_Weather = safeuuid();
container.innerHTML = `
container.innerHTML = html`
<h1>Gestión del Aula</h1>
<div>
<fieldset style="float: left;">
<legend><img src="${PAGES.notas.icon}" height="20"> Notas esenciales</legend>
<a class="button" style="font-size: 25px;" href="#notas,inicio_dia">Como iniciar el día</a>
<a class="button" style="font-size: 25px;" href="#notas,realizacion_cafe">Como realizar el café</a>
<a class="button" style="font-size: 25px;" href="#notas,fin_dia">Como acabar el día</a>
<a class="button" style="font-size: 25px;" href="#notas,horario">Horario</a>
<a class="button" style="font-size: 25px;" href="#notas,tareas">Tareas</a>
<legend><img src="${PAGES.notas.icon}" height="20" /> Notas esenciales</legend>
<a class="button" style="font-size: 25px;" href="#notas,inicio_dia"
>Como iniciar el día</a
>
<a class="button" style="font-size: 25px;" href="#notas,realizacion_cafe"
>Como realizar el café</a
>
<a class="button" style="font-size: 25px;" href="#notas,fin_dia">Como acabar el día</a>
<a class="button" style="font-size: 25px;" href="#notas,horario">Horario</a>
<a class="button" style="font-size: 25px;" href="#notas,tareas">Tareas</a>
</fieldset>
<fieldset style="float: left;">
<legend>Acciones</legend>
<a class="button" style="font-size: 25px;" href="#aulas,solicitudes"><img src="${PAGES.materiales.icon}" height="20"> Solicitudes de material</a>
<a class="button" style="font-size: 25px;" href="#aulas,informes,diario-${CurrentISODate()}">Diario de hoy</a>
<a class="button rojo" style="font-size: 25px;" href="#notas,alertas"><img src="${PAGES.notas.icon}" height="20"> Ver Alertas</a>
<a class="button" style="font-size: 25px;" href="#aulas,informes"><img src="${PAGES.aulas.icon}" height="20"> Informes y diarios</a>
<a class="button btn4" style="font-size: 25px;" href="#supercafe"><img src="${PAGES.supercafe.icon}" height="20"> Ver comandas</a>
<legend>Acciones</legend>
<a class="button" style="font-size: 25px;" href="#aulas,solicitudes"
><img src="${PAGES.materiales.icon}" height="20" /> Solicitudes de material</a
>
<a
class="button"
style="font-size: 25px;"
href="#aulas,informes,diario-${CurrentISODate()}"
>Diario de hoy</a
>
<a class="button rojo" style="font-size: 25px;" href="#notas,alertas"
><img src="${PAGES.notas.icon}" height="20" /> Ver Alertas</a
>
<a class="button" style="font-size: 25px;" href="#aulas,informes"
><img src="${PAGES.aulas.icon}" height="20" /> Informes y diarios</a
>
<a class="button btn4" style="font-size: 25px;" href="#supercafe"
><img src="${PAGES.supercafe.icon}" height="20" /> Ver comandas</a
>
</fieldset>
<fieldset style="float: left;">
<legend>Datos de hoy</legend>
<span class="btn7" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"><b>Menú Comedor:</b> <br><span id="${data_Comedor}">Cargando...</span></span>
<span class="btn6" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"><b>Tareas:</b> <br><pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Tareas}">Cargando...</pre></span>
<span class="btn5" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"><b>Diario:</b> <br><pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Diario}">Cargando...</pre></span>
<span class="btn4" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"><b>Clima:</b> <br><img loading="lazy" style="padding: 15px; background-color: white; width: 245px;" id="${data_Weather}"></span>
<legend>Datos de hoy</legend>
<span
class="btn7"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"
><b>Menú Comedor:</b> <br /><span id="${data_Comedor}">Cargando...</span></span
>
<span
class="btn6"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"
><b>Tareas:</b> <br />
<pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Tareas}">
Cargando...</pre
>
</span>
<span
class="btn5"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"
><b>Diario:</b> <br />
<pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Diario}">
Cargando...</pre
>
</span>
<span
class="btn4"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"
><b>Clima:</b> <br /><img
loading="lazy"
style="padding: 15px; background-color: white; width: 245px;"
id="${data_Weather}"
/></span>
</fieldset>
</div>
`;
`;
//#region Cargar Clima
// Get location from DB settings.weather_location; if missing ask user and save it
// url format: https://wttr.in/<loc>?F0m
DB.get('settings','weather_location').then((loc) => {
DB.get('settings', 'weather_location').then((loc) => {
if (!loc) {
loc = prompt("Introduce tu ubicación para el clima (ciudad, país):", "Madrid, Spain");
loc = prompt('Introduce tu ubicación para el clima (ciudad, país):', 'Madrid, Spain');
if (loc) {
DB.put('settings','weather_location', loc);
DB.put('settings', 'weather_location', loc);
}
}
if (loc) {
document.getElementById(data_Weather).src = "https://wttr.in/" + encodeURIComponent(loc) + "_IF0m_background=FFFFFF.png";
document.getElementById(data_Weather).src =
'https://wttr.in/' + encodeURIComponent(loc) + '_IF0m_background=FFFFFF.png';
} else {
document.getElementById(data_Weather).src = "https://wttr.in/_IF0m_background=FFFFFF.png";
document.getElementById(data_Weather).src = 'https://wttr.in/_IF0m_background=FFFFFF.png';
}
});
//#endregion Cargar Clima
@@ -65,17 +107,20 @@ PAGES.aulas = {
DB.get('comedor', CurrentISODate()).then((data) => {
function add_row(data) {
// Fix newlines
data.Platos = data.Platos || "No hay platos registrados para hoy.";
data.Platos = data.Platos || 'No hay platos registrados para hoy.';
// Display platos
document.getElementById(data_Comedor).innerHTML = data.Platos.replace(
/\n/g,
"<br>"
);
document.getElementById(data_Comedor).innerHTML = data.Platos.replace(/\n/g, '<br>');
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
add_row(data || {});
}, 'comedor', CurrentISODate());
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'comedor',
CurrentISODate()
);
} else {
add_row(data || {});
}
@@ -85,17 +130,20 @@ PAGES.aulas = {
DB.get('notas', 'tareas').then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || "No hay tareas.";
data.Contenido = data.Contenido || 'No hay tareas.';
// Display platos
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(
/\n/g,
"<br>"
);
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(/\n/g, '<br>');
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
add_row(data || {});
}, 'notas', 'tareas');
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'notas',
'tareas'
);
} else {
add_row(data || {});
}
@@ -105,17 +153,20 @@ PAGES.aulas = {
DB.get('aulas_informes', 'diario-' + CurrentISODate()).then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || "No hay un diario.";
data.Contenido = data.Contenido || 'No hay un diario.';
// Display platos
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(
/\n/g,
"<br>"
);
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(/\n/g, '<br>');
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
add_row(data || {});
}, 'aulas_informes', 'diario-' + CurrentISODate());
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'aulas_informes',
'diario-' + CurrentISODate()
);
} else {
add_row(data || {});
}
@@ -125,33 +176,33 @@ PAGES.aulas = {
_solicitudes: function () {
const tablebody = safeuuid();
var btn_new = safeuuid();
container.innerHTML = `
container.innerHTML = html`
<a class="button" href="#aulas">← Volver a Gestión de Aulas</a>
<h1>Solicitudes de Material</h1>
<button id="${btn_new}">Nueva solicitud</button>
<div id="cont"></div>
`;
`;
TS_IndexElement(
"aulas,solicitudes",
'aulas,solicitudes',
[
{
key: "Solicitante",
type: "persona",
default: "",
label: "Solicitante",
key: 'Solicitante',
type: 'persona',
default: '',
label: 'Solicitante',
},
{
key: "Asunto",
type: "raw",
default: "",
label: "Asunto",
key: 'Asunto',
type: 'raw',
default: '',
label: 'Asunto',
},
],
"aulas_solicitudes",
document.querySelector("#cont")
'aulas_solicitudes',
document.querySelector('#cont')
);
document.getElementById(btn_new).onclick = () => {
setUrlHash("aulas,solicitudes," + safeuuid(""));
setUrlHash('aulas,solicitudes,' + safeuuid(''));
};
},
_solicitudes__edit: function (mid) {
@@ -161,39 +212,46 @@ PAGES.aulas = {
var field_autor = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
container.innerHTML = `
<a class="button" href="#aulas,solicitudes">← Volver a solicitudes</a>
<h1>Solicitud <code id="${nameh1}"></code></h1>
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
<legend>Valores</legend>
<div style="max-width: 400px;">
<label>
Asunto<br>
<input type="text" id="${field_asunto}" value=""><br><br>
</label>
<input type="hidden" id="${field_autor}" readonly value="">
</div>
<label>
Contenido - ¡Incluye el material a solicitar!<br>
<textarea id="${field_contenido}" style="width: 100%; height: 400px;"></textarea><br><br>
</label>
<hr>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
container.innerHTML = html`
<a class="button" href="#aulas,solicitudes">← Volver a solicitudes</a>
<h1>Solicitud <code id="${nameh1}"></code></h1>
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
<legend>Valores</legend>
<div style="max-width: 400px;">
<label>
Asunto<br />
<input type="text" id="${field_asunto}" value="" /><br /><br />
</label>
<input type="hidden" id="${field_autor}" readonly value="" />
</div>
<label>
Contenido - ¡Incluye el material a solicitar!<br />
<textarea id="${field_contenido}" style="width: 100%; height: 400px;"></textarea
><br /><br />
</label>
<hr />
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
(async () => {
const data = await DB.get('aulas_solicitudes', mid);
function load_data(data, ENC = "") {
function load_data(data, ENC = '') {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_asunto).value = data["Asunto"] || "";
document.getElementById(field_contenido).value = data["Contenido"] || "";
document.getElementById(field_autor).value = data["Solicitante"] || SUB_LOGGED_IN_ID || "";
document.getElementById(field_asunto).value = data['Asunto'] || '';
document.getElementById(field_contenido).value = data['Contenido'] || '';
document.getElementById(field_autor).value = data['Solicitante'] || SUB_LOGGED_IN_ID || '';
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
load_data(data, "%E");
}, 'aulas_solicitudes', mid);
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
load_data(data, '%E');
},
'aulas_solicitudes',
mid
);
} else {
load_data(data || {});
}
@@ -202,36 +260,38 @@ PAGES.aulas = {
// Disable button to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
guardarBtn.disabled = true;
guardarBtn.style.opacity = "0.5";
guardarBtn.style.opacity = '0.5';
var data = {
Solicitante: document.getElementById(field_autor).value,
Contenido: document.getElementById(field_contenido).value,
Asunto: document.getElementById(field_asunto).value,
};
document.getElementById("actionStatus").style.display = "block";
DB.put('aulas_solicitudes', mid, data).then(() => {
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("aulas,solicitudes");
}, SAVE_WAIT);
}).catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = "1";
document.getElementById("actionStatus").style.display = "none";
toastr.error("Error al guardar la solicitud");
});
document.getElementById('actionStatus').style.display = 'block';
DB.put('aulas_solicitudes', mid, data)
.then(() => {
toastr.success('Guardado!');
setTimeout(() => {
document.getElementById('actionStatus').style.display = 'none';
setUrlHash('aulas,solicitudes');
}, SAVE_WAIT);
})
.catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = '1';
document.getElementById('actionStatus').style.display = 'none';
toastr.error('Error al guardar la solicitud');
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm("¿Quieres borrar esta solicitud?") == true) {
if (confirm('¿Quieres borrar esta solicitud?') == true) {
DB.del('aulas_solicitudes', mid).then(() => {
toastr.error("Borrado!");
toastr.error('Borrado!');
setTimeout(() => {
setUrlHash("aulas,solicitudes");
setUrlHash('aulas,solicitudes');
}, SAVE_WAIT);
});
}
@@ -242,53 +302,56 @@ PAGES.aulas = {
var btn_new = safeuuid();
var field_new_byday = safeuuid();
var btn_new_byday = safeuuid();
container.innerHTML = `
container.innerHTML = html`
<a class="button" href="#aulas">← Volver a Gestión de Aulas</a>
<h1>Informes</h1>
<div style="display: inline-block; border: 2px solid black; padding: 5px; border-radius: 5px;">
<b>Diario:</b><br>
<input type="date" id="${field_new_byday}" value="${CurrentISODate()}">
<button id="${btn_new_byday}">Abrir / Nuevo</button>
</div><br>
<div
style="display: inline-block; border: 2px solid black; padding: 5px; border-radius: 5px;"
>
<b>Diario:</b><br />
<input type="date" id="${field_new_byday}" value="${CurrentISODate()}" />
<button id="${btn_new_byday}">Abrir / Nuevo</button>
</div>
<br />
<button id="${btn_new}">Nuevo informe</button>
<div id="cont"></div>
`;
`;
TS_IndexElement(
"aulas,informes",
'aulas,informes',
[
{
key: "Autor",
type: "persona",
default: "",
label: "Autor",
key: 'Autor',
type: 'persona',
default: '',
label: 'Autor',
},
{
key: "Fecha",
type: "fecha",
default: "",
label: "Fecha",
key: 'Fecha',
type: 'fecha',
default: '',
label: 'Fecha',
},
{
key: "Asunto",
type: "raw",
default: "",
label: "Asunto",
key: 'Asunto',
type: 'raw',
default: '',
label: 'Asunto',
},
],
"aulas_informes",
document.querySelector("#cont")
'aulas_informes',
document.querySelector('#cont')
);
document.getElementById(btn_new).onclick = () => {
setUrlHash("aulas,informes," + safeuuid(""));
setUrlHash('aulas,informes,' + safeuuid(''));
};
document.getElementById(btn_new_byday).onclick = () => {
const day = document.getElementById(field_new_byday).value;
if (day) {
setUrlHash("aulas,informes,diario-" + day);
setUrlHash('aulas,informes,diario-' + day);
} else {
toastr.error("Selecciona un día válido");
toastr.error('Selecciona un día válido');
}
}
};
},
_informes__edit: function (mid) {
var nameh1 = safeuuid();
@@ -298,45 +361,49 @@ PAGES.aulas = {
var field_fecha = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var title = "";
if (mid.startsWith("diario-")) {
var date = mid.replace("diario-", "").split("-");
title = "Diario " + date[2] + "/" + date[1] + "/" + date[0];
var title = '';
if (mid.startsWith('diario-')) {
var date = mid.replace('diario-', '').split('-');
title = 'Diario ' + date[2] + '/' + date[1] + '/' + date[0];
}
container.innerHTML = `
container.innerHTML = html`
<a class="button" href="#aulas,informes">← Volver a informes</a>
<h1>Informe <code id="${nameh1}"></code></h1>
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
<legend>Valores</legend>
<div style="max-width: 400px;">
<label>
Asunto<br>
<input type="text" id="${field_asunto}" value=""><br><br>
</label>
<input type="hidden" id="${field_autor}" readonly value="">
<input type="hidden" id="${field_fecha}" value="">
</div>
<legend>Valores</legend>
<div style="max-width: 400px;">
<label>
Contenido<br>
<textarea id="${field_contenido}" style="width: 100%; height: 400px;"></textarea><br><br>
Asunto<br />
<input type="text" id="${field_asunto}" value="" /><br /><br />
</label>
<hr>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
<input type="hidden" id="${field_autor}" readonly value="" />
<input type="hidden" id="${field_fecha}" value="" />
</div>
<label>
Contenido<br />
<textarea id="${field_contenido}" style="width: 100%; height: 400px;"></textarea
><br /><br />
</label>
<hr />
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
`;
(async () => {
const data = await DB.get('aulas_informes', mid);
function load_data(data, ENC = "") {
function load_data(data, ENC = '') {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_asunto).value = data["Asunto"] || title || "";
document.getElementById(field_contenido).value = data["Contenido"] || "";
document.getElementById(field_autor).value = data["Autor"] || SUB_LOGGED_IN_ID || "";
document.getElementById(field_fecha).value = data["Fecha"] || mid.startsWith("diario-") ? mid.replace("diario-", "") : CurrentISODate();
document.getElementById(field_asunto).value = data['Asunto'] || title || '';
document.getElementById(field_contenido).value = data['Contenido'] || '';
document.getElementById(field_autor).value = data['Autor'] || SUB_LOGGED_IN_ID || '';
document.getElementById(field_fecha).value =
data['Fecha'] || mid.startsWith('diario-')
? mid.replace('diario-', '')
: CurrentISODate();
}
if (typeof data == "string") {
if (typeof data == 'string') {
TS_decrypt(data, SECRET, (data) => {
load_data(data, "%E");
load_data(data, '%E');
});
} else {
load_data(data || {});
@@ -346,55 +413,57 @@ PAGES.aulas = {
// Disable button to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
guardarBtn.disabled = true;
guardarBtn.style.opacity = "0.5";
guardarBtn.style.opacity = '0.5';
var data = {
Autor: document.getElementById(field_autor).value,
Contenido: document.getElementById(field_contenido).value,
Asunto: document.getElementById(field_asunto).value,
Fecha: document.getElementById(field_fecha).value || CurrentISODate(),
};
document.getElementById("actionStatus").style.display = "block";
DB.put('aulas_informes', mid, data).then(() => {
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("aulas,informes");
}, SAVE_WAIT);
}).catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = "1";
document.getElementById("actionStatus").style.display = "none";
toastr.error("Error al guardar el informe");
});
document.getElementById('actionStatus').style.display = 'block';
DB.put('aulas_informes', mid, data)
.then(() => {
toastr.success('Guardado!');
setTimeout(() => {
document.getElementById('actionStatus').style.display = 'none';
setUrlHash('aulas,informes');
}, SAVE_WAIT);
})
.catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = '1';
document.getElementById('actionStatus').style.display = 'none';
toastr.error('Error al guardar el informe');
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm("¿Quieres borrar este informe?") == true) {
if (confirm('¿Quieres borrar este informe?') == true) {
DB.del('aulas_informes', mid).then(() => {
toastr.error("Borrado!");
toastr.error('Borrado!');
setTimeout(() => {
setUrlHash("aulas,informes");
setUrlHash('aulas,informes');
}, SAVE_WAIT);
});
}
};
},
edit: function (section) {
if (!checkRole("aulas")) {
setUrlHash("index");
if (!checkRole('aulas')) {
setUrlHash('index');
return;
}
var item = location.hash.replace("#", "").split(",")[2];
var item = location.hash.replace('#', '').split(',')[2];
if (!item) {
// No item, show section
switch (section) {
case "solicitudes":
case 'solicitudes':
this._solicitudes();
break;
case "informes":
case 'informes':
this._informes();
break;
default:
@@ -404,10 +473,10 @@ PAGES.aulas = {
} else {
// Show section__edit
switch (section) {
case "solicitudes":
case 'solicitudes':
this._solicitudes__edit(item);
break;
case "informes":
case 'informes':
this._informes__edit(item);
break;
}

View File

@@ -1,234 +1,247 @@
PERMS["avisos"] = "Avisos"
PERMS["avisos:edit"] = "&gt; Editar"
PERMS['avisos'] = 'Avisos';
PERMS['avisos:edit'] = '&gt; Editar';
PAGES.avisos = {
navcss: "btn5",
icon: "static/appico/File_Plugin.svg",
AccessControl: true,
Title: "Avisos",
edit: function (mid) {
if (!checkRole("avisos:edit")) {setUrlHash("avisos");return}
var nameh1 = safeuuid();
var field_fecha = safeuuid();
var field_asunto = safeuuid();
var field_origen = safeuuid();
var field_destino = safeuuid();
var field_estado = safeuuid();
var field_mensaje = safeuuid();
var field_respuesta = safeuuid();
var btn_leer = safeuuid();
var btn_desleer = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var div_actions = safeuuid();
container.innerHTML = `
<h1>Aviso <code id="${nameh1}"></code></h1>
<fieldset style="float: left;">
<legend>Valores</legend>
<label>
Fecha<br>
<input readonly disabled type="text" id="${field_fecha}" value=""><br><br>
</label>
<label>
Asunto<br>
<input type="text" id="${field_asunto}" value=""><br><br>
</label>
<input type="hidden" id="${field_origen}">
<input type="hidden" id="${field_destino}">
<div id="${div_actions}"></div>
<label>
Mensaje<br>
<textarea id="${field_mensaje}"></textarea><br><br>
</label>
<label>
Respuesta<br>
<textarea id="${field_respuesta}"></textarea><br><br>
</label>
<label>
Estado<br>
<input readonly disabled type="text" id="${field_estado}" value="">
<br>
<button id="${btn_leer}">Leido</button>
<button id="${btn_desleer}">No leido</button>
<br>
</label><hr>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
document.getElementById(btn_leer).onclick = () => {
document.getElementById(field_estado).value = "leido";
};
document.getElementById(btn_desleer).onclick = () => {
document.getElementById(field_estado).value = "por_leer";
};
var divact = document.getElementById(div_actions);
addCategory_Personas(
divact,
SC_Personas,
"",
(value) => {
document.getElementById(field_origen).value = value;
},
"Origen"
);
addCategory_Personas(
divact,
SC_Personas,
"",
(value) => {
document.getElementById(field_destino).value = value;
},
"Destino"
);
(async () => {
const data = await DB.get('notificaciones', mid);
function load_data(data, ENC = "") {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_fecha).value = data["Fecha"] || CurrentISODate() || "";
document.getElementById(field_asunto).value = data["Asunto"] || "";
document.getElementById(field_mensaje).value = data["Mensaje"] || "";
document.getElementById(field_origen).value = data["Origen"] || SUB_LOGGED_IN_ID || "";
document.getElementById(field_destino).value = data["Destino"] || "";
document.getElementById(field_estado).value = data["Estado"] || "%%" || "";
document.getElementById(field_respuesta).value = data["Respuesta"] || "";
navcss: 'btn5',
icon: 'static/appico/File_Plugin.svg',
AccessControl: true,
Title: 'Avisos',
edit: function (mid) {
if (!checkRole('avisos:edit')) {
setUrlHash('avisos');
return;
}
var nameh1 = safeuuid();
var field_fecha = safeuuid();
var field_asunto = safeuuid();
var field_origen = safeuuid();
var field_destino = safeuuid();
var field_estado = safeuuid();
var field_mensaje = safeuuid();
var field_respuesta = safeuuid();
var btn_leer = safeuuid();
var btn_desleer = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var div_actions = safeuuid();
container.innerHTML = html`
<h1>Aviso <code id="${nameh1}"></code></h1>
<fieldset style="float: left;">
<legend>Valores</legend>
<label>
Fecha<br />
<input readonly disabled type="text" id="${field_fecha}" value="" /><br /><br />
</label>
<label>
Asunto<br />
<input type="text" id="${field_asunto}" value="" /><br /><br />
</label>
<input type="hidden" id="${field_origen}" />
<input type="hidden" id="${field_destino}" />
<div id="${div_actions}"></div>
<label>
Mensaje<br />
<textarea id="${field_mensaje}"></textarea><br /><br />
</label>
<label>
Respuesta<br />
<textarea id="${field_respuesta}"></textarea><br /><br />
</label>
<label>
Estado<br />
<input readonly disabled type="text" id="${field_estado}" value="" />
<br />
<button id="${btn_leer}">Leido</button>
<button id="${btn_desleer}">No leido</button>
<br />
</label>
<hr />
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
document.getElementById(btn_leer).onclick = () => {
document.getElementById(field_estado).value = 'leido';
};
document.getElementById(btn_desleer).onclick = () => {
document.getElementById(field_estado).value = 'por_leer';
};
var divact = document.getElementById(div_actions);
addCategory_Personas(
divact,
SC_Personas,
'',
(value) => {
document.getElementById(field_origen).value = value;
},
'Origen'
);
addCategory_Personas(
divact,
SC_Personas,
'',
(value) => {
document.getElementById(field_destino).value = value;
},
'Destino'
);
(async () => {
const data = await DB.get('notificaciones', mid);
function load_data(data, ENC = '') {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_fecha).value = data['Fecha'] || CurrentISODate() || '';
document.getElementById(field_asunto).value = data['Asunto'] || '';
document.getElementById(field_mensaje).value = data['Mensaje'] || '';
document.getElementById(field_origen).value = data['Origen'] || SUB_LOGGED_IN_ID || '';
document.getElementById(field_destino).value = data['Destino'] || '';
document.getElementById(field_estado).value = data['Estado'] || '%%' || '';
document.getElementById(field_respuesta).value = data['Respuesta'] || '';
// Persona select
divact.innerHTML = "";
addCategory_Personas(
divact,
SC_Personas,
data["Origen"] || "",
(value) => {
document.getElementById(field_origen).value = value;
},
"Origen"
);
addCategory_Personas(
divact,
SC_Personas,
data["Destino"] || "",
(value) => {
document.getElementById(field_destino).value = value;
},
"Destino"
);
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
load_data(data, "%E");
}, 'notificaciones', mid);
} else {
load_data(data || {});
}
})();
document.getElementById(btn_guardar).onclick = () => {
// Check if button is already disabled to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
// Validate before disabling button
if (document.getElementById(field_origen).value == "") {
alert("¡Hay que elegir una persona de origen!");
return;
}
if (document.getElementById(field_destino).value == "") {
alert("¡Hay que elegir una persona de origen!");
return;
}
// Disable button after validation passes
guardarBtn.disabled = true;
guardarBtn.style.opacity = "0.5";
var data = {
Fecha: document.getElementById(field_fecha).value,
Origen: document.getElementById(field_origen).value,
Destino: document.getElementById(field_destino).value,
Mensaje: document.getElementById(field_mensaje).value,
Respuesta: document.getElementById(field_respuesta).value,
Asunto: document.getElementById(field_asunto).value,
Estado: document
.getElementById(field_estado)
.value.replace("%%", "por_leer"),
};
document.getElementById("actionStatus").style.display = "block";
DB.put('notificaciones', mid, data).then(() => {
toastr.success("Guardado!");
// Persona select
divact.innerHTML = '';
addCategory_Personas(
divact,
SC_Personas,
data['Origen'] || '',
(value) => {
document.getElementById(field_origen).value = value;
},
'Origen'
);
addCategory_Personas(
divact,
SC_Personas,
data['Destino'] || '',
(value) => {
document.getElementById(field_destino).value = value;
},
'Destino'
);
}
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
load_data(data, '%E');
},
'notificaciones',
mid
);
} else {
load_data(data || {});
}
})();
document.getElementById(btn_guardar).onclick = () => {
// Check if button is already disabled to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
// Validate before disabling button
if (document.getElementById(field_origen).value == '') {
alert('¡Hay que elegir una persona de origen!');
return;
}
if (document.getElementById(field_destino).value == '') {
alert('¡Hay que elegir una persona de origen!');
return;
}
// Disable button after validation passes
guardarBtn.disabled = true;
guardarBtn.style.opacity = '0.5';
var data = {
Fecha: document.getElementById(field_fecha).value,
Origen: document.getElementById(field_origen).value,
Destino: document.getElementById(field_destino).value,
Mensaje: document.getElementById(field_mensaje).value,
Respuesta: document.getElementById(field_respuesta).value,
Asunto: document.getElementById(field_asunto).value,
Estado: document.getElementById(field_estado).value.replace('%%', 'por_leer'),
};
document.getElementById('actionStatus').style.display = 'block';
DB.put('notificaciones', mid, data)
.then(() => {
toastr.success('Guardado!');
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("avisos");
document.getElementById('actionStatus').style.display = 'none';
setUrlHash('avisos');
}, SAVE_WAIT);
}).catch((e) => {
})
.catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = "1";
document.getElementById("actionStatus").style.display = "none";
toastr.error("Error al guardar la notificación");
guardarBtn.style.opacity = '1';
document.getElementById('actionStatus').style.display = 'none';
toastr.error('Error al guardar la notificación');
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm('¿Quieres borrar esta notificación?') == true) {
DB.del('notificaciones', mid).then(() => {
toastr.error('Borrado!');
setTimeout(() => {
setUrlHash('avisos');
}, SAVE_WAIT);
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm("¿Quieres borrar esta notificación?") == true) {
DB.del('notificaciones', mid).then(() => {
toastr.error("Borrado!");
setTimeout(() => {
setUrlHash("avisos");
}, SAVE_WAIT);
});
}
};
},
index: function () {
if (!checkRole("avisos")) {setUrlHash("index");return}
const tablebody = safeuuid();
var btn_new = safeuuid();
container.innerHTML = `
<h1>Avisos</h1>
<button id="${btn_new}">Nuevo aviso</button>
<div id="cont"></div>
`;
TS_IndexElement(
"avisos",
[
{
key: "Origen",
type: "persona",
default: "",
label: "Origen",
},
{
key: "Destino",
type: "persona",
default: "",
label: "Destino",
},
{
key: "Asunto",
type: "raw",
default: "",
label: "Asunto",
},
{
key: "Estado",
type: "raw",
default: "",
label: "Estado",
},
],
"notificaciones",
document.querySelector("#cont"),
(data, new_tr) => {
new_tr.style.backgroundColor = "#FFCCCB";
if (data.Estado == "leido") {
new_tr.style.backgroundColor = "lightgreen";
}
}
);
if (!checkRole("avisos:edit")) {
document.getElementById(btn_new).style.display = "none"
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash("avisos," + safeuuid(""));
};
}
},
}
};
},
index: function () {
if (!checkRole('avisos')) {
setUrlHash('index');
return;
}
const tablebody = safeuuid();
var btn_new = safeuuid();
container.innerHTML = html`
<h1>Avisos</h1>
<button id="${btn_new}">Nuevo aviso</button>
<div id="cont"></div>
`;
TS_IndexElement(
'avisos',
[
{
key: 'Origen',
type: 'persona',
default: '',
label: 'Origen',
},
{
key: 'Destino',
type: 'persona',
default: '',
label: 'Destino',
},
{
key: 'Asunto',
type: 'raw',
default: '',
label: 'Asunto',
},
{
key: 'Estado',
type: 'raw',
default: '',
label: 'Estado',
},
],
'notificaciones',
document.querySelector('#cont'),
(data, new_tr) => {
new_tr.style.backgroundColor = '#FFCCCB';
if (data.Estado == 'leido') {
new_tr.style.backgroundColor = 'lightgreen';
}
}
);
if (!checkRole('avisos:edit')) {
document.getElementById(btn_new).style.display = 'none';
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash('avisos,' + safeuuid(''));
};
}
},
};

View File

@@ -1,7 +1,7 @@
PAGES.buscar = {
navcss: "btn1",
icon: "static/appico/view.svg",
Title: "Buscar",
navcss: 'btn1',
icon: 'static/appico/view.svg',
Title: 'Buscar',
AccessControl: true,
Esconder: true,
@@ -12,32 +12,33 @@ PAGES.buscar = {
const recentSearches = safeuuid();
const moduleFilter = safeuuid();
container.innerHTML = `
container.innerHTML = html`
<h1>🔍 Búsqueda Global</h1>
<p>Busca en todos los módulos: personas, materiales, café, comedor, notas y avisos</p>
<fieldset>
<legend>Opciones de búsqueda</legend>
<input type="text" id="${searchInput}"
placeholder="Escribe aquí para buscar..."
onkeypress="if(event.key==='Enter') document.getElementById('${searchButton}').click()">
<input
type="text"
id="${searchInput}"
placeholder="Escribe aquí para buscar..."
onkeypress="if(event.key==='Enter') document.getElementById('${searchButton}').click()"
/>
<select id="${moduleFilter}">
<option value="">Todos los módulos</option>
<!-- Options will be populated dynamically based on user permissions -->
</select>
<button id="${searchButton}" class="btn5">
Buscar
</button>
<button id="${searchButton}" class="btn5">Buscar</button>
</fieldset>
<div id="${recentSearches}"></div>
<div id="${resultsContainer}">
<fieldset>
<legend>Resultados</legend>
<div>🔍 Introduce un término de búsqueda para comenzar</div>
<p>Puedes buscar por nombres, referencias, fechas, ubicaciones...</p>
<details>
<summary>💡 Consejos de búsqueda</summary>
<ul>
@@ -72,7 +73,7 @@ PAGES.buscar = {
// Add only accessible modules
accessibleModules.forEach((module) => {
const option = document.createElement("option");
const option = document.createElement('option');
option.value = module.key;
option.textContent = `${getModuleIcon(module.key)} ${module.title}`;
moduleFilterEl.appendChild(option);
@@ -82,24 +83,22 @@ PAGES.buscar = {
// Helper function to get module icons (fallback for older module mappings)
function getModuleIcon(moduleKey) {
const iconMap = {
personas: "👤",
materiales: "📦",
supercafe: "☕",
comedor: "🍽️",
avisos: "🔔",
aulas: "🏫",
resumen_diario: "📊",
personas: '👤',
materiales: '📦',
supercafe: '☕',
comedor: '🍽️',
avisos: '🔔',
aulas: '🏫',
resumen_diario: '📊',
};
return iconMap[moduleKey] || "📋";
return iconMap[moduleKey] || '📋';
}
// Load recent searches from localStorage
function loadRecentSearches() {
const recent = JSON.parse(
localStorage.getItem("telesec_recent_searches") || "[]"
);
const recent = JSON.parse(localStorage.getItem('telesec_recent_searches') || '[]');
if (recent.length > 0) {
recentSearchesEl.innerHTML = `
recentSearchesEl.innerHTML = html`
<fieldset>
<legend>Búsquedas recientes</legend>
${recent
@@ -110,8 +109,11 @@ PAGES.buscar = {
</button>
`
)
.join("")}
<button onclick="localStorage.removeItem('telesec_recent_searches'); this.parentElement.style.display='none';" class="rojo">
.join('')}
<button
onclick="localStorage.removeItem('telesec_recent_searches'); this.parentElement.style.display='none';"
class="rojo"
>
Limpiar
</button>
</fieldset>
@@ -126,14 +128,12 @@ PAGES.buscar = {
function saveToRecent(term) {
if (!term || term.length < 2) return;
let recent = JSON.parse(
localStorage.getItem("telesec_recent_searches") || "[]"
);
let recent = JSON.parse(localStorage.getItem('telesec_recent_searches') || '[]');
recent = recent.filter((t) => t !== term); // Remove if exists
recent.unshift(term); // Add to beginning
recent = recent.slice(0, 5); // Keep only 5 most recent
localStorage.setItem("telesec_recent_searches", JSON.stringify(recent));
localStorage.setItem('telesec_recent_searches', JSON.stringify(recent));
loadRecentSearches();
}
@@ -143,7 +143,7 @@ PAGES.buscar = {
const selectedModule = moduleFilterEl.value;
if (searchTerm.length < 2) {
resultsEl.innerHTML = `
resultsEl.innerHTML = html`
<fieldset>
<legend>Error</legend>
<div>⚠️ Por favor, introduce al menos 2 caracteres para buscar</div>
@@ -153,7 +153,7 @@ PAGES.buscar = {
}
// Show loading
resultsEl.innerHTML = `
resultsEl.innerHTML = html`
<fieldset>
<legend>Buscando...</legend>
<div>⏳ Procesando búsqueda...</div>
@@ -166,9 +166,7 @@ PAGES.buscar = {
// Filter by module if selected
if (selectedModule) {
results = results.filter(
(result) => result._module === selectedModule
);
results = results.filter((result) => result._module === selectedModule);
}
globalSearch.renderResults(results, resultsEl);
@@ -176,16 +174,17 @@ PAGES.buscar = {
// Add stats
if (results.length > 0) {
const statsDiv = document.createElement("fieldset");
const legend = document.createElement("legend");
legend.textContent = "Estadísticas";
const statsDiv = document.createElement('fieldset');
const legend = document.createElement('legend');
legend.textContent = 'Estadísticas';
statsDiv.appendChild(legend);
let filterText = selectedModule
? ` en ${moduleFilterEl.options[moduleFilterEl.selectedIndex].text}`
: "";
const content = document.createElement("div");
content.innerHTML = `📊 Se encontraron <strong>${results.length}</strong> resultados para "<strong>${searchTerm}</strong>"${filterText}`;
: '';
const content = document.createElement('div');
content.innerHTML = html`📊 Se encontraron <strong>${results.length}</strong> resultados
para "<strong>${searchTerm}</strong>"${filterText}`;
statsDiv.appendChild(content);
resultsEl.insertBefore(statsDiv, resultsEl.firstChild);
@@ -218,24 +217,24 @@ PAGES.buscar = {
searchInputEl.focus();
// Add keyboard shortcuts
document.addEventListener("keydown", function (e) {
document.addEventListener('keydown', function (e) {
// Ctrl+F or Cmd+F to focus search
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
searchInputEl.focus();
searchInputEl.select();
}
// Escape to clear search
if (e.key === "Escape") {
searchInputEl.value = "";
if (e.key === 'Escape') {
searchInputEl.value = '';
searchInputEl.focus();
resultsEl.innerHTML = `
resultsEl.innerHTML = html`
<fieldset>
<legend>Resultados</legend>
<div>🔍 Introduce un término de búsqueda para comenzar</div>
<p>Puedes buscar por nombres, referencias, fechas, ubicaciones...</p>
<details>
<summary>💡 Consejos de búsqueda</summary>
<ul>
@@ -252,10 +251,10 @@ PAGES.buscar = {
});
// Check for quick search term from header
const quickSearchTerm = sessionStorage.getItem("telesec_quick_search");
const quickSearchTerm = sessionStorage.getItem('telesec_quick_search');
if (quickSearchTerm) {
searchInputEl.value = quickSearchTerm;
sessionStorage.removeItem("telesec_quick_search");
sessionStorage.removeItem('telesec_quick_search');
// Perform search automatically
setTimeout(performSearch, 100);
}

View File

@@ -1,44 +1,52 @@
PERMS["comedor"] = "Comedor"
PERMS["comedor:edit"] = "&gt; Editar"
PERMS['comedor'] = 'Comedor';
PERMS['comedor:edit'] = '&gt; Editar';
PAGES.comedor = {
navcss: "btn6",
icon: "static/appico/apple.png",
navcss: 'btn6',
icon: 'static/appico/apple.png',
AccessControl: true,
Title: "Comedor",
Title: 'Comedor',
edit: function (mid) {
if (!checkRole("comedor:edit")) {setUrlHash("comedor");return}
if (!checkRole('comedor:edit')) {
setUrlHash('comedor');
return;
}
var nameh1 = safeuuid();
var field_fecha = safeuuid();
var field_platos = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
container.innerHTML = `
container.innerHTML = html`
<h1>Entrada del menú <code id="${nameh1}"></code></h1>
<fieldset style="float: left;">
<legend>Valores</legend>
<label>
Fecha<br>
<input type="date" id="${field_fecha}" value=""><br><br>
Fecha<br />
<input type="date" id="${field_fecha}" value="" /><br /><br />
</label>
<label>
Platos<br>
<textarea id="${field_platos}"></textarea><br><br>
Platos<br />
<textarea id="${field_platos}"></textarea><br /><br />
</label>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
`;
DB.get('comedor', mid).then((data) => {
function load_data(data, ENC = "") {
function load_data(data, ENC = '') {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_fecha).value = data["Fecha"] || mid || CurrentISODate();
document.getElementById(field_platos).value =
data["Platos"] || "";
document.getElementById(field_fecha).value = data['Fecha'] || mid || CurrentISODate();
document.getElementById(field_platos).value = data['Platos'] || '';
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
load_data(data, "%E");
}, 'comedor', mid);
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
load_data(data, '%E');
},
'comedor',
mid
);
} else {
load_data(data || {});
}
@@ -47,88 +55,93 @@ PAGES.comedor = {
// Disable button to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
guardarBtn.disabled = true;
guardarBtn.style.opacity = "0.5";
guardarBtn.style.opacity = '0.5';
const newDate = document.getElementById(field_fecha).value;
var data = {
Fecha: newDate,
Platos: document.getElementById(field_platos).value,
};
// If the date has changed, we need to delete the old entry
if (mid !== newDate && mid !== "") {
if (mid !== newDate && mid !== '') {
DB.del('comedor', mid);
}
document.getElementById("actionStatus").style.display = "block";
DB.put('comedor', newDate, data).then(() => {
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("comedor");
}, SAVE_WAIT);
}).catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = "1";
document.getElementById("actionStatus").style.display = "none";
toastr.error("Error al guardar el menú");
});
document.getElementById('actionStatus').style.display = 'block';
DB.put('comedor', newDate, data)
.then(() => {
toastr.success('Guardado!');
setTimeout(() => {
document.getElementById('actionStatus').style.display = 'none';
setUrlHash('comedor');
}, SAVE_WAIT);
})
.catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = '1';
document.getElementById('actionStatus').style.display = 'none';
toastr.error('Error al guardar el menú');
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm("¿Quieres borrar esta entrada?") == true) {
if (confirm('¿Quieres borrar esta entrada?') == true) {
DB.del('comedor', mid).then(() => {
toastr.error("Borrado!");
toastr.error('Borrado!');
setTimeout(() => {
setUrlHash("comedor");
setUrlHash('comedor');
}, SAVE_WAIT);
});
}
};
},
index: function () {
if (!checkRole("comedor")) {setUrlHash("index");return}
if (!checkRole('comedor')) {
setUrlHash('index');
return;
}
const cont = safeuuid();
var btn_new = safeuuid();
container.innerHTML = `
container.innerHTML = html`
<h1>Menú del comedor</h1>
<button id="${btn_new}">Nueva entrada</button>
<div id="${cont}"></div>
`;
`;
TS_IndexElement(
"comedor",
'comedor',
[
{
key: "Fecha",
type: "raw",
default: "",
label: "Fecha",
key: 'Fecha',
type: 'raw',
default: '',
label: 'Fecha',
},
{
key: "Platos",
type: "raw",
default: "",
label: "Platos",
}
key: 'Platos',
type: 'raw',
default: '',
label: 'Platos',
},
],
"comedor",
'comedor',
document.getElementById(cont),
(data, new_tr) => {
// new_tr.style.backgroundColor = "#FFCCCB";
if (data.Fecha == CurrentISODate()) {
new_tr.style.backgroundColor = "lightgreen";
new_tr.style.backgroundColor = 'lightgreen';
}
}
);
if (!checkRole("comedor:edit")) {
document.getElementById(btn_new).style.display = "none"
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash("comedor," + safeuuid(""));
};
}
if (!checkRole('comedor:edit')) {
document.getElementById(btn_new).style.display = 'none';
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash('comedor,' + safeuuid(''));
};
}
},
};

View File

@@ -1,20 +1,20 @@
PAGES.dataman = {
navcss: "btn1",
icon: "static/appico/gear_edit.png",
navcss: 'btn1',
icon: 'static/appico/gear_edit.png',
AccessControl: true,
Title: "Ajustes",
Title: 'Ajustes',
edit: function (mid) {
switch (mid) {
case "export":
case 'export':
PAGES.dataman.__export();
break;
case "import":
case 'import':
PAGES.dataman.__import();
break;
case "config":
case 'config':
PAGES.dataman.__config();
break;
case "labels":
case 'labels':
PAGES.dataman.__labels();
break;
default:
@@ -23,22 +23,22 @@ PAGES.dataman = {
},
__config: function () {
var form = safeuuid();
container.innerHTML = `
<h1>Ajustes</h1>
<h2>No disponible</h2>
<form id="${form}">
<label>
<input type="checkbox" name="block_add_account" value="yes">
<b>Bloquear crear cuenta de administrador?</b>
</label>
<button type="submit">Aplicar ajustes</button>
</form>
container.innerHTML = html`
<h1>Ajustes</h1>
<h2>No disponible</h2>
<form id="${form}">
<label>
<input type="checkbox" name="block_add_account" value="yes" />
<b>Bloquear crear cuenta de administrador?</b>
</label>
<button type="submit">Aplicar ajustes</button>
</form>
`;
document.getElementById(form).onsubmit = (ev) => {
ev.preventDefault();
var ford = new FormData(document.getElementById(form));
if (ford.get("block_add_account") == "yes") {
config["block_add_account"] = true;
if (ford.get('block_add_account') == 'yes') {
config['block_add_account'] = true;
}
};
},
@@ -49,19 +49,21 @@ PAGES.dataman = {
var button_export_safe = safeuuid();
var button_export_safe_cloud = safeuuid();
var button_clear = safeuuid();
container.innerHTML = `
container.innerHTML = html`
<h1>Exportar Datos</h1>
<fieldset>
<legend>Exportar datos</legend>
<em>Al pulsar, Espera hasta que salga una notificacion verde.</em>
<br>
<br>
<br />
<br />
<button id="${button_export_local}" type="button">Exportar sin cifrar</button>
<button id="${button_export_safe}" type="button">Exportar con cifrado</button>
<button id="${button_export_safe_cloud}" style="display: none;" type="button">Exportar a EuskadiTech - cifrado</button>
<button id="${button_export_safe_cloud}" style="display: none;" type="button">
Exportar a EuskadiTech - cifrado
</button>
<!--<br><br><em>Para descargar envia un correo a telesec@tech.eus con el asunto "TSBK %${getDBName()}".</em>-->
</fieldset>
`;
`;
document.getElementById(button_export_local).onclick = () => {
var data_export = {};
var output = {
@@ -70,52 +72,62 @@ PAGES.dataman = {
};
(async () => {
const materiales = await DB.list('materiales');
materiales.forEach(entry => {
materiales.forEach((entry) => {
const key = entry.id;
const value = entry.data;
if (value != null) {
if (typeof value == 'string') {
TS_decrypt(value, SECRET, (data, wasEncrypted) => {
output.materiales[key] = data;
}, 'materiales', key);
TS_decrypt(
value,
SECRET,
(data, wasEncrypted) => {
output.materiales[key] = data;
},
'materiales',
key
);
} else {
output.materiales[key] = value;
}
}
});
const personas = await DB.list('personas');
personas.forEach(entry => {
personas.forEach((entry) => {
const key = entry.id;
const value = entry.data;
if (value != null) {
if (typeof value == 'string') {
TS_decrypt(value, SECRET, (data, wasEncrypted) => {
output.personas[key] = data;
}, 'personas', key);
TS_decrypt(
value,
SECRET,
(data, wasEncrypted) => {
output.personas[key] = data;
},
'personas',
key
);
} else {
output.personas[key] = value;
}
}
});
toastr.success("Exportado todo, descargando!");
download(
`Export %%TITLE%% ${getDBName()}.json.txt`,
JSON.stringify(output)
);
toastr.success('Exportado todo, descargando!');
download(`Export %%TITLE%% ${getDBName()}.json.txt`, JSON.stringify(output));
})();
};
document.getElementById(button_export_safe).onclick = () => {
(async () => {
const result = { materiales: {}, personas: {} };
const materiales = await DB.list('materiales');
materiales.forEach(entry => { result.materiales[entry.id] = entry.data; });
materiales.forEach((entry) => {
result.materiales[entry.id] = entry.data;
});
const personas = await DB.list('personas');
personas.forEach(entry => { result.personas[entry.id] = entry.data; });
toastr.success("Exportado todo, descargado!");
download(
`Export %%TITLE%% Encriptado ${getDBName()}.json.txt`,
JSON.stringify(result)
);
personas.forEach((entry) => {
result.personas[entry.id] = entry.data;
});
toastr.success('Exportado todo, descargado!');
download(`Export %%TITLE%% Encriptado ${getDBName()}.json.txt`, JSON.stringify(result));
})();
};
// document.getElementById(button_export_safe_cloud).onclick = () => {
@@ -143,30 +155,34 @@ PAGES.dataman = {
var textarea_content = safeuuid();
var button_import = safeuuid();
var button_clear = safeuuid();
container.innerHTML = `
container.innerHTML = html`
<h1>Importar Datos</h1>
<fieldset>
<legend>Importar datos</legend>
<em>Espera hasta que se vacien todas las notificaciones.</em>
<select id="${select_type}">
<option value="" disabled selected>Tipo de archivo...</option>
<option value="comedor">Galileo - db.comedor.axd</option>
<option value="recetas">Galileo - db.recetas.axd</option>
<option value="materiales">Galileo - db.materiales.axd</option>
<option value="personas">Galileo - db.personas.axd</option>
<option value="comandas">Galileo - db.cafe.comandas.axd</option>
<option value="%telesec">TeleSec Exportado (encriptado o no)</option>
</select>
<textarea id="${textarea_content}" style="height: 100px;" placeholder="Introduce el contenido del archivo"></textarea>
<button id="${button_import}" type="button">Importar</button>
<button id="${button_clear}" type="button">Vaciar</button>
<legend>Importar datos</legend>
<em>Espera hasta que se vacien todas las notificaciones.</em>
<select id="${select_type}">
<option value="" disabled selected>Tipo de archivo...</option>
<option value="comedor">Galileo - db.comedor.axd</option>
<option value="recetas">Galileo - db.recetas.axd</option>
<option value="materiales">Galileo - db.materiales.axd</option>
<option value="personas">Galileo - db.personas.axd</option>
<option value="comandas">Galileo - db.cafe.comandas.axd</option>
<option value="%telesec">TeleSec Exportado (encriptado o no)</option>
</select>
<textarea
id="${textarea_content}"
style="height: 100px;"
placeholder="Introduce el contenido del archivo"
></textarea>
<button id="${button_import}" type="button">Importar</button>
<button id="${button_clear}" type="button">Vaciar</button>
</fieldset>
`;
`;
document.getElementById(button_import).onclick = () => {
toastr.info("Importando datos...");
toastr.info('Importando datos...');
var val = document.getElementById(textarea_content).value;
var sel = document.getElementById(select_type).value;
if (sel == "%telesec") {
if (sel == '%telesec') {
// legacy import, store entire payload as-is
// for each top-level key, store their items in DB
var parsed = JSON.parse(val);
@@ -174,19 +190,23 @@ PAGES.dataman = {
const sectionName = section[0];
const sectionData = section[1];
Object.entries(sectionData).forEach((entry) => {
DB.put(sectionName, entry[0], entry[1]).catch((e) => { console.warn('DB.put error', e); });
DB.put(sectionName, entry[0], entry[1]).catch((e) => {
console.warn('DB.put error', e);
});
});
});
} else {
Object.entries(JSON.parse(val)["data"]).forEach((entry) => {
DB.put(sel, entry[0], entry[1]).catch((e) => { console.warn('DB.put error', e); });
Object.entries(JSON.parse(val)['data']).forEach((entry) => {
DB.put(sel, entry[0], entry[1]).catch((e) => {
console.warn('DB.put error', e);
});
});
}
setTimeout(() => {
toastr.info("Importado todo!");
toastr.info('Importado todo!');
if (sel == "%telesec") {
setUrlHash("inicio");
if (sel == '%telesec') {
setUrlHash('inicio');
} else {
setUrlHash(sel);
}
@@ -195,38 +215,34 @@ PAGES.dataman = {
},
__labels: function (mid) {
var div_materiales = safeuuid();
container.innerHTML = `
<h1>Imprimir Etiquetas QR</h1>
container.innerHTML = html` <h1>Imprimir Etiquetas QR</h1>
<button onclick="print()">Imprimir</button>
<h2>Materiales</h2>
<div id="${div_materiales}"></div>
<br><br>`;
<br /><br />`;
div_materiales = document.getElementById(div_materiales);
DB.map('materiales', (data, key) => {
function add_row(data, key) {
if (data != null) {
div_materiales.innerHTML += BuildQR(
"materiales," + key,
data["Nombre"] || key
);
div_materiales.innerHTML += BuildQR('materiales,' + key, data['Nombre'] || key);
}
}
if (typeof data == "string") {
if (typeof data == 'string') {
TS_decrypt(data, SECRET, (data) => {
add_row(data, key);
});
} else {
add_row(data, key);
}
});
});
} else {
add_row(data, key);
}
});
},
index: function () {
container.innerHTML = `
<h1>Administración de datos</h1>
<a class="button" href="#dataman,import">Importar datos</a>
<a class="button" href="#dataman,export">Exportar datos</a>
<a class="button" href="#dataman,labels">Imprimir etiquetas</a>
<a class="button" href="#dataman,config">Ajustes</a>
container.innerHTML = html`
<h1>Administración de datos</h1>
<a class="button" href="#dataman,import">Importar datos</a>
<a class="button" href="#dataman,export">Exportar datos</a>
<a class="button" href="#dataman,labels">Imprimir etiquetas</a>
<a class="button" href="#dataman,config">Ajustes</a>
`;
},
};

View File

@@ -1,44 +1,44 @@
PAGES.index = {
//navcss: "btn1",
Title: "Inicio",
icon: "static/appico/house.png",
index: function() {
container.innerHTML = `
<h1>¡Hola, ${SUB_LOGGED_IN_DETAILS.Nombre}!<br>Bienvenidx a %%TITLE%%</h1>
<h2>Tienes ${parseFloat(SUB_LOGGED_IN_DETAILS.Monedero_Balance).toPrecision(2)} € en el monedero.</h2>
Title: 'Inicio',
icon: 'static/appico/house.png',
index: function () {
container.innerHTML = html`
<h1>¡Hola, ${SUB_LOGGED_IN_DETAILS.Nombre}!<br />Bienvenidx a %%TITLE%%</h1>
<h2>
Tienes ${parseFloat(SUB_LOGGED_IN_DETAILS.Monedero_Balance).toPrecision(2)} € en el
monedero.
</h2>
<em>Utiliza el menú superior para abrir un modulo</em>
<br><br>
<br /><br />
<button class="btn1" onclick="LogOutTeleSec()">Cerrar sesión</button>
`;
},
edit: function(mid) {
edit: function (mid) {
switch (mid) {
case 'qr':
PAGES.index.__scan()
PAGES.index.__scan();
break;
}
},
__scan: function(mid) {
var qrscan = safeuuid()
container.innerHTML = `
<h1>Escanear Codigo QR</h1>
__scan: function (mid) {
var qrscan = safeuuid();
container.innerHTML = html` <h1>Escanear Codigo QR</h1>
<div style="max-width: 400px;" id="${qrscan}"></div>
<br><br>`;
var html5QrcodeScanner = new Html5QrcodeScanner(
qrscan, { fps: 10, qrbox: 250 });
<br /><br />`;
var html5QrcodeScanner = new Html5QrcodeScanner(qrscan, { fps: 10, qrbox: 250 });
function onScanSuccess(decodedText, decodedResult) {
html5QrcodeScanner.clear();
// Handle on success condition with the decoded text or result.
// alert(`Scan result: ${decodedText}`, decodedResult);
setUrlHash(decodedText)
setUrlHash(decodedText);
// ...
// ^ this will stop the scanner (video feed) and clear the scan area.
}
html5QrcodeScanner.render(onScanSuccess);
EventListeners.QRScanner.push(html5QrcodeScanner)
}
}
EventListeners.QRScanner.push(html5QrcodeScanner);
},
};

View File

@@ -1,505 +1,461 @@
function makeCouchURLDisplay(host, user, pass, dbname) {
if (!host) return '';
var display = user + ':' + pass + '@' + host.replace(/^https?:\/\//, '') + '/' + dbname;
return display;
}
PAGES.login = {
Esconder: true,
Title: "Login",
onboarding: function (step) {
// Multi-step onboarding flow
step = step || 'config';
if (step === 'config') {
// Step 1: "Configuración de datos"
var field_couch = safeuuid();
var field_couch_dbname = safeuuid();
var field_couch_user = safeuuid();
var field_couch_pass = safeuuid();
var field_secret = safeuuid();
var field_server_preset = safeuuid();
var btn_existing_server = safeuuid();
var btn_new_server = safeuuid();
var btn_skip = safeuuid();
var div_server_config = safeuuid();
container.innerHTML = `
<h1>¡Bienvenido a TeleSec! 🎉</h1>
<h2>Paso 1: Configuración de datos</h2>
<p>Para comenzar, elige cómo quieres configurar tu base de datos:</p>
Esconder: true,
Title: 'Login',
onboarding: function (step) {
// Multi-step onboarding flow
step = step || 'config';
if (step === 'config') {
// Step 1: "Configuración de datos"
var field_couch = safeuuid();
var field_secret = safeuuid();
var btn_existing_server = safeuuid();
var btn_new_server = safeuuid();
var btn_skip = safeuuid();
var div_server_config = safeuuid();
container.innerHTML = html`
<h1>¡Bienvenido a TeleSec! 🎉</h1>
<h2>Paso 1: Configuración de datos</h2>
<p>Para comenzar, elige cómo quieres configurar tu base de datos:</p>
<fieldset>
<button id="${btn_existing_server}" class="btn5">Conectar a CouchDB existente</button>
<button id="${btn_new_server}" class="btn2">Solicitar un nuevo CouchDB</button>
<button id="${btn_skip}" class="btn3">No sincronizar (no recomendado)</button>
</fieldset>
<div id="${div_server_config}" style="display:none;margin-top:20px;">
<h3>Configuración del servidor CouchDB</h3>
<fieldset>
<button id="${btn_existing_server}" class="btn5" style="margin:10px;padding:15px;">📡 Conectar a un servidor CouchDB existente</button>
<button id="${btn_new_server}" class="btn2" style="margin:10px;padding:15px;">🆕 Crear un nuevo servidor (registro externo)</button>
<button id="${btn_skip}" class="btn3" style="margin:10px;padding:15px;">⏭️ Saltar (usar solo local)</button>
<label
>Origen CouchDB (ej: usuario:contraseña@servidor/basededatos)
<input
type="text"
id="${field_couch}"
value="${makeCouchURLDisplay(
localStorage.getItem('TELESEC_COUCH_URL'),
localStorage.getItem('TELESEC_COUCH_USER'),
localStorage.getItem('TELESEC_COUCH_PASS'),
localStorage.getItem('TELESEC_COUCH_DBNAME')
)}"
/><br /><br />
</label>
<label
>Clave de encriptación <span style="color: red;">*</span>
<input
type="password"
id="${field_secret}"
value="${localStorage.getItem('TELESEC_SECRET') || ''}"
required
/><br /><br />
</label>
<button id="${btn_skip}-save" class="btn5">Guardar y Continuar</button>
</fieldset>
<div id="${div_server_config}" style="display:none;margin-top:20px;">
<h3>Configuración del servidor CouchDB</h3>
<fieldset>
<label>Servidor predefinido (opcional)
<select id="${field_server_preset}" style="width:100%;padding:8px;margin-bottom:10px;">
<option value="">-- Selecciona un servidor o introduce uno manualmente --</option>
<option value="b.tech.eus">b.tech.eus - EuskadiTech B1</option>
<option value="c.tech.eus">c.tech.eus - EuskadiTech C1</option>
</select>
</label>
<label>Servidor CouchDB (ej: couch.example.com)
<input type="text" id="${field_couch}" value="${(localStorage.getItem('TELESEC_COUCH_URL') || '').replace(/^https?:\/\//, '')}"><br><br>
</label>
<label>Nombre de la base (opcional)
<input type="text" id="${field_couch_dbname}" value="${localStorage.getItem('TELESEC_COUCH_DBNAME') || ''}"><br><br>
</label>
<label>Usuario
<input type="text" id="${field_couch_user}" value="${localStorage.getItem('TELESEC_COUCH_USER') || ''}"><br><br>
</label>
<label>Contraseña
<input type="password" id="${field_couch_pass}" value="${localStorage.getItem('TELESEC_COUCH_PASS') || ''}"><br><br>
</label>
<label>Clave de encriptación <span style="color: red;">*</span>
<input type="password" id="${field_secret}" value="${localStorage.getItem('TELESEC_SECRET') || ''}" required><br><br>
</label>
<button id="${btn_skip}-save" class="btn5">Guardar y Continuar</button>
</fieldset>
</div>
`;
document.getElementById(btn_existing_server).onclick = () => {
document.getElementById(div_server_config).style.display = 'block';
};
// Server preset selector handler
document.getElementById(field_server_preset).onchange = (e) => {
var preset = e.target.value;
if (preset) {
document.getElementById(field_couch).value = preset;
}
};
document.getElementById(btn_new_server).onclick = () => {
window.open('https://tech.eus/telesec-signup.php', '_blank');
toastr.info('Una vez creado el servidor, vuelve aquí y conéctate usando el botón "Conectar a un servidor existente"');
};
document.getElementById(btn_skip).onclick = () => {
// Continue to persona creation without server config
// Check if personas already exist (shouldn't happen but safety check)
var hasPersonas = Object.keys(SC_Personas).length > 0;
if (hasPersonas) {
toastr.info('Ya existen personas. Saltando creación de cuenta.');
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
open_page('login');
setUrlHash('login');
} else {
open_page('login,onboarding-persona');
setUrlHash('login,onboarding-persona');
}
};
document.getElementById(btn_skip + '-save').onclick = () => {
var url = document.getElementById(field_couch).value.trim();
var dbname = document.getElementById(field_couch_dbname).value.trim();
var user = document.getElementById(field_couch_user).value.trim();
var pass = document.getElementById(field_couch_pass).value;
var secret = document.getElementById(field_secret).value.trim();
if (!url) {
toastr.error('Por favor ingresa un servidor CouchDB');
return;
}
if (!secret) {
toastr.error('La clave de encriptación es obligatoria');
return;
}
// Normalize URL: add https:// if no protocol specified
var normalizedUrl = url;
if (!/^https?:\/\//i.test(url)) {
normalizedUrl = 'https://' + url;
}
localStorage.setItem('TELESEC_COUCH_URL', normalizedUrl);
localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
localStorage.setItem('TELESEC_COUCH_USER', user);
localStorage.setItem('TELESEC_COUCH_PASS', pass);
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
SECRET = secret.toUpperCase();
try {
DB.init({ secret: SECRET, remoteServer: normalizedUrl, username: user, password: pass, dbname: dbname || undefined });
toastr.success('Servidor configurado correctamente');
// Wait a moment for initial replication to pull personas
setTimeout(() => {
// Check if personas were replicated from server
var hasPersonas = Object.keys(SC_Personas).length > 0;
if (hasPersonas) {
// Personas found from server, skip persona creation step
toastr.info('Se encontraron personas en el servidor. Saltando creación de cuenta.');
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
open_page('login');
setUrlHash('login');
} else {
// No personas found, continue to persona creation
open_page('login,onboarding-persona');
setUrlHash('login,onboarding-persona');
}
}, 2000);
} catch (e) {
toastr.error('Error al configurar el servidor: ' + (e.message || e));
}
};
} else if (step === 'persona') {
// Step 2: "Crea una persona"
var field_nombre = safeuuid();
var btn_crear = safeuuid();
// Check if personas already exist
</div>
`;
document.getElementById(btn_existing_server).onclick = () => {
document.getElementById(div_server_config).style.display = 'block';
};
document.getElementById(btn_new_server).onclick = () => {
window.open('https://tech.eus/telesec-signup.php', '_blank');
toastr.info(
'Una vez creado el servidor, vuelve aquí y conéctate usando el botón "Conectar a un servidor existente"'
);
};
document.getElementById(btn_skip).onclick = () => {
// Continue to persona creation without server config
// Check if personas already exist (shouldn't happen but safety check)
var hasPersonas = Object.keys(SC_Personas).length > 0;
if (hasPersonas) {
toastr.info('Se detectaron personas existentes. Redirigiendo al login.');
toastr.info('Ya existen personas. Saltando creación de cuenta.');
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
open_page('login');
setUrlHash('login');
} else {
open_page('login,onboarding-persona');
setUrlHash('login,onboarding-persona');
}
};
document.getElementById(btn_skip + '-save').onclick = () => {
var url = document.getElementById(field_couch).value.trim();
var secret = document.getElementById(field_secret).value.trim();
if (!url) {
toastr.error('Por favor ingresa un servidor CouchDB');
return;
}
container.innerHTML = `
<h1>¡Bienvenido a TeleSec! 🎉</h1>
<h2>Paso 2: Crea tu cuenta de administrador</h2>
<p>Para continuar, necesitas crear una cuenta personal con permisos de administrador.</p>
<fieldset>
<label>Tu nombre:
<input type="text" id="${field_nombre}" placeholder="Ej: Juan Pérez" autofocus><br><br>
</label>
<p><small> Esta cuenta tendrá todos los permisos de administrador y podrás gestionar la aplicación completamente.</small></p>
<button id="${btn_crear}" class="btn5">Crear cuenta y empezar</button>
</fieldset>
`;
document.getElementById(btn_crear).onclick = () => {
var nombre = document.getElementById(field_nombre).value.trim();
if (!nombre) {
toastr.error('Por favor ingresa tu nombre');
return;
if (!secret) {
toastr.error('La clave de encriptación es obligatoria');
return;
}
// Normalize URL: add https:// if no protocol specified
var normalizedUrl = url;
if (!/^https?:\/\//i.test(url)) {
normalizedUrl = 'https://' + url;
}
var URL_PARSED = parseURL(normalizedUrl);
var user = URL_PARSED.username || '';
var pass = URL_PARSED.password || '';
var dbname = URL_PARSED.pathname ? URL_PARSED.pathname.replace(/^\//, '') : '';
var host = URL_PARSED.hostname || normalizedUrl;
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + host);
localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
localStorage.setItem('TELESEC_COUCH_USER', user);
localStorage.setItem('TELESEC_COUCH_PASS', pass);
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
SECRET = secret.toUpperCase();
try {
DB.init({
secret: SECRET,
remoteServer: 'https://' + host,
username: user,
password: pass,
dbname: dbname || undefined,
});
toastr.success('Servidor configurado correctamente');
document.getElementById('loading').style.display = 'block';
function waitForReplicationIdle(maxWaitMs, idleMs) {
var startTime = Date.now();
var lastSeenSync = window.TELESEC_LAST_SYNC || 0;
return new Promise((resolve) => {
var interval = setInterval(() => {
var now = Date.now();
var currentSync = window.TELESEC_LAST_SYNC || 0;
if (currentSync > lastSeenSync) {
lastSeenSync = currentSync;
}
var lastActivity = Math.max(lastSeenSync, startTime);
var idleLongEnough = now - lastActivity >= idleMs;
var timedOut = now - startTime >= maxWaitMs;
if (idleLongEnough || timedOut) {
clearInterval(interval);
resolve();
}
}, 250);
});
}
// Disable button to prevent duplicate creation
var btnElement = document.getElementById(btn_crear);
btnElement.disabled = true;
btnElement.style.opacity = '0.5';
btnElement.innerText = 'Creando...';
// Create persona with all admin permissions from PERMS object
var allPerms = Object.keys(PERMS).join(',') + ',';
var personaId = safeuuid('admin-');
var persona = {
Nombre: nombre,
Roles: allPerms,
Region: '',
Monedero_Balance: 0,
markdown: 'Cuenta de administrador creada durante el onboarding'
};
DB.put('personas', personaId, persona).then(() => {
// Wait until replication goes idle or timeout
waitForReplicationIdle(10000, 2500).then(() => {
// Check if personas were replicated from server
var hasPersonas = Object.keys(SC_Personas).length > 0;
document.getElementById('loading').style.display = 'none';
if (hasPersonas) {
// Personas found from server, skip persona creation step
toastr.info('Se encontraron personas en el servidor. Saltando creación de cuenta.');
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
open_page('login');
setUrlHash('login');
} else {
// No personas found, continue to persona creation
open_page('login,onboarding-persona');
setUrlHash('login,onboarding-persona');
}
});
} catch (e) {
document.getElementById('loading').style.display = 'none';
toastr.error('Error al configurar el servidor: ' + (e.message || e));
}
};
} else if (step === 'persona') {
// Step 2: "Crea una persona"
var field_nombre = safeuuid();
var btn_crear = safeuuid();
// Check if personas already exist
var hasPersonas = Object.keys(SC_Personas).length > 0;
if (hasPersonas) {
toastr.info('Se detectaron personas existentes. Redirigiendo al login.');
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
open_page('login');
setUrlHash('login');
return;
}
container.innerHTML = html`
<h1>¡Bienvenido a TeleSec! 🎉</h1>
<h2>Paso 2: Crea tu cuenta de administrador</h2>
<p>Para continuar, necesitas crear una cuenta personal con permisos de administrador.</p>
<fieldset>
<label
>Tu nombre:
<input
type="text"
id="${field_nombre}"
placeholder="Ej: Juan Pérez"
autofocus
/><br /><br />
</label>
<p>
<small
> Esta cuenta tendrá todos los permisos de administrador y podrás gestionar la
aplicación completamente.</small
>
</p>
<button id="${btn_crear}" class="btn5">Crear cuenta y empezar</button>
</fieldset>
`;
document.getElementById(btn_crear).onclick = () => {
var nombre = document.getElementById(field_nombre).value.trim();
if (!nombre) {
toastr.error('Por favor ingresa tu nombre');
return;
}
// Disable button to prevent duplicate creation
var btnElement = document.getElementById(btn_crear);
btnElement.disabled = true;
btnElement.style.opacity = '0.5';
btnElement.innerText = 'Creando...';
// Create persona with all admin permissions from PERMS object
var allPerms = Object.keys(PERMS).join(',') + ',';
var personaId = safeuuid('admin-');
var persona = {
Nombre: nombre,
Roles: allPerms,
Region: '',
Monedero_Balance: 0,
markdown: 'Cuenta de administrador creada durante el onboarding',
};
DB.put('personas', personaId, persona)
.then(() => {
toastr.success('¡Cuenta creada exitosamente! 🎉');
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
localStorage.setItem('TELESEC_ADMIN_ID', personaId);
// Auto-login
SUB_LOGGED_IN_ID = personaId;
SUB_LOGGED_IN_DETAILS = persona;
SUB_LOGGED_IN = true;
SetPages();
setTimeout(() => {
open_page('index');
setUrlHash('index');
}, 500);
}).catch((e) => {
})
.catch((e) => {
toastr.error('Error creando cuenta: ' + (e.message || e));
// Re-enable button on error
btnElement.disabled = false;
btnElement.style.opacity = '1';
btnElement.innerText = 'Crear cuenta y empezar';
});
};
}
},
edit: function (mid) {
// Handle onboarding routes
if (mid === 'onboarding-config') {
PAGES.login.onboarding('config');
return;
}
if (mid === 'onboarding-persona') {
PAGES.login.onboarding('persona');
return;
}
// Setup form to configure CouchDB remote and initial group/secret
var field_couch = safeuuid();
var field_couch_dbname = safeuuid();
var field_couch_user = safeuuid();
var field_couch_pass = safeuuid();
var field_secret = safeuuid();
var btn_import_json = safeuuid();
var div_import_area = safeuuid();
var field_json = safeuuid();
var field_file = safeuuid();
var btn_parse_json = safeuuid();
var btn_start_scan = safeuuid();
var div_scan = safeuuid();
var btn_save = safeuuid();
container.innerHTML = `
<h1>Configuración del servidor CouchDB</h1>
<b>Aviso: Después de guardar, la aplicación intentará sincronizar con el servidor CouchDB en segundo plano. Puede que falten registros hasta que se termine. Tenga paciencia.</b>
<fieldset>
<label>Servidor CouchDB (ej: couch.example.com)
<input type="text" id="${field_couch}" value="${(localStorage.getItem('TELESEC_COUCH_URL') || '').replace(/^https?:\/\//, '')}"><br><br>
</label>
<label>Nombre de la base (opcional, por defecto usa telesec-<grupo>)
<input type="text" id="${field_couch_dbname}" value="${localStorage.getItem('TELESEC_COUCH_DBNAME') || ''}"><br><br>
</label>
<label>Usuario
<input type="text" id="${field_couch_user}" value="${localStorage.getItem('TELESEC_COUCH_USER') || ''}"><br><br>
</label>
<label>Contraseña
<input type="password" id="${field_couch_pass}" value="${localStorage.getItem('TELESEC_COUCH_PASS') || ''}"><br><br>
</label>
<label>Clave de encriptación (opcional) - usada para cifrar datos en reposo
<input type="password" id="${field_secret}" value="${localStorage.getItem('TELESEC_SECRET') || ''}"><br><br>
</label>
<div style="margin-top:8px;">
<button id="${btn_import_json}" class="btn4">Importar desde JSON / QR</button>
</div>
<div id="${div_import_area}" style="display:none;margin-top:10px;border:1px solid #eee;padding:8px;">
<label>Pegar JSON de configuración (o usar archivo / QR):</label><br>
<textarea id="${field_json}" style="width:100%;height:120px;margin-top:6px;" placeholder='{"server":"couch.example.com","dbname":"telesec-test","username":"user","password":"pass","secret":"SECRET123"}'></textarea>
<div style="margin-top:6px;">
<input type="file" id="${field_file}" accept="application/json">
<button id="${btn_parse_json}" class="btn5">Aplicar JSON</button>
<button id="${btn_start_scan}" class="btn3">Escanear QR (si disponible)</button>
</div>
<div id="${div_scan}" style="margin-top:8px;"></div>
</div>
<button id="${btn_save}" class="btn5">Guardar y Conectar</button>
</fieldset>
<p>Después de guardar, el navegador intentará sincronizar en segundo plano con el servidor.</p>
`;
// Helper: normalize and apply config object
function applyConfig(cfg) {
try {
if (!cfg) throw new Error('JSON vacío');
var url = cfg.server || cfg.couch || cfg.url || cfg.host || cfg.hostname || cfg.server_url;
var dbname = cfg.dbname || cfg.database || cfg.db || cfg.name;
var user = cfg.username || cfg.user || cfg.u;
var pass = cfg.password || cfg.pass || cfg.p;
var secret = (cfg.secret || cfg.key || cfg.secretKey || cfg.SECRET || '').toString();
if (!url) throw new Error('Falta campo "server" en JSON');
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + url.replace(/^https?:\/\//, ''));
if (dbname) localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
if (user) localStorage.setItem('TELESEC_COUCH_USER', user);
if (pass) localStorage.setItem('TELESEC_COUCH_PASS', pass);
if (secret) {
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
SECRET = secret.toUpperCase();
}
DB.init({ secret: SECRET, remoteServer: 'https://' + url.replace(/^https?:\/\//, ''), username: user, password: pass, dbname: dbname || undefined });
toastr.success('Configuración aplicada e iniciando sincronización');
location.hash = '#login';
setTimeout(function(){ location.reload(); }, 400);
} catch (e) {
toastr.error('Error aplicando configuración: ' + (e && e.message ? e.message : e));
}
};
}
},
edit: function (mid) {
// Handle onboarding routes
if (mid === 'onboarding-config') {
PAGES.login.onboarding('config');
return;
}
if (mid === 'onboarding-persona') {
PAGES.login.onboarding('persona');
return;
}
// Setup form to configure CouchDB remote and initial group/secret
var field_couch = safeuuid();
var field_secret = safeuuid();
var btn_save = safeuuid();
container.innerHTML = html`
<h1>Configuración del servidor CouchDB</h1>
<b
>Aviso: Después de guardar, la aplicación intentará sincronizar con el servidor CouchDB en
segundo plano. Puede que falten registros hasta que se termine. Tenga paciencia.</b
>
<fieldset>
<label
>Origen CouchDB (ej: usuario:contraseña@servidor/basededatos)
<input
type="text"
id="${field_couch}"
value="${makeCouchURLDisplay(
localStorage.getItem('TELESEC_COUCH_URL'),
localStorage.getItem('TELESEC_COUCH_USER'),
localStorage.getItem('TELESEC_COUCH_PASS'),
localStorage.getItem('TELESEC_COUCH_DBNAME')
)}"
/><br /><br />
</label>
<label
>Clave de encriptación (opcional) - usada para cifrar datos en reposo
<input
type="password"
id="${field_secret}"
value="${localStorage.getItem('TELESEC_SECRET') || ''}"
/><br /><br />
</label>
<button id="${btn_save}" class="btn5">Guardar y Conectar</button>
<button onclick="setUrlHash('login');" class="btn3">Cancelar</button>
</fieldset>
<p>
Después de guardar, el navegador intentará sincronizar en segundo plano con el servidor.
</p>
`;
// Helper: normalize and apply config object
function applyConfig(cfg) {
try {
if (!cfg) throw new Error('JSON vacío');
var url = cfg.server || cfg.couch || cfg.url || cfg.host || cfg.hostname || cfg.server_url;
var dbname = cfg.dbname || cfg.database || cfg.db || cfg.name;
var user = cfg.username || cfg.user || cfg.u;
var pass = cfg.password || cfg.pass || cfg.p;
var secret = (cfg.secret || cfg.key || cfg.secretKey || cfg.SECRET || '').toString();
if (!url) throw new Error('Falta campo "server" en JSON');
var URL_PARSED = parseURL(url);
var host = URL_PARSED.hostname || url;
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + host);
if (dbname) localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
if (user) localStorage.setItem('TELESEC_COUCH_USER', user);
if (pass) localStorage.setItem('TELESEC_COUCH_PASS', pass);
if (secret) {
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
SECRET = secret.toUpperCase();
}
// Toggle import area
document.getElementById(btn_import_json).onclick = function () {
var el = document.getElementById(div_import_area);
el.style.display = (el.style.display === 'none') ? 'block' : 'none';
};
// Parse textarea JSON
document.getElementById(btn_parse_json).onclick = function () {
var txt = document.getElementById(field_json).value.trim();
if (!txt) { toastr.error('JSON vacío'); return; }
try {
var obj = JSON.parse(txt);
applyConfig(obj);
} catch (e) {
toastr.error('JSON inválido: ' + e.message);
}
};
// File input: read JSON file and apply
document.getElementById(field_file).addEventListener('change', function (ev) {
var f = ev.target.files && ev.target.files[0];
if (!f) return;
var r = new FileReader();
r.onload = function (e) {
try {
var txt = e.target.result;
document.getElementById(field_json).value = txt;
var obj = JSON.parse(txt);
applyConfig(obj);
} catch (err) {
toastr.error('Error leyendo archivo JSON: ' + (err && err.message ? err.message : err));
}
};
r.readAsText(f);
DB.init({
secret: SECRET,
remoteServer: 'https://' + url.replace(/^https?:\/\//, ''),
username: user,
password: pass,
dbname: dbname || undefined,
});
// QR scanning (if html5-qrcode available)
document.getElementById(btn_start_scan).onclick = function () {
var scanDiv = document.getElementById(div_scan);
scanDiv.innerHTML = '';
if (window.Html5QrcodeScanner || window.Html5Qrcode) {
try {
var targetId = div_scan + '-cam';
scanDiv.innerHTML = '<div id="' + targetId + '"></div><div style="margin-top:6px;"><button id="' + targetId + '-stop" class="btn3">Detener</button></div>';
var html5Qr;
if (window.Html5Qrcode) {
html5Qr = new Html5Qrcode(targetId);
Html5Qrcode.getCameras().then(function(cameras){
var camId = (cameras && cameras[0] && cameras[0].id) ? cameras[0].id : undefined;
html5Qr.start({ facingMode: 'environment' }, { fps: 10, qrbox: 250 }, function(decodedText){
try {
var obj = JSON.parse(decodedText);
html5Qr.stop();
applyConfig(obj);
} catch (e) {
toastr.error('QR no contiene JSON válido');
}
}, function(err){ /* ignore scan errors */ }).catch(function(err){ toastr.error('Error iniciando cámara: ' + err); });
}).catch(function(){
// fallback: start without camera list
html5Qr.start({ facingMode: 'environment' }, { fps: 10, qrbox: 250 }, function(decodedText){
try { applyConfig(JSON.parse(decodedText)); } catch(e){ toastr.error('QR no contiene JSON válido'); }
}, function(){}).catch(function(err){
toastr.error('Error iniciando cámara: ' + (err && err.message ? err.message : err));
});
});
} else {
// Html5QrcodeScanner fallback
var scanner = new Html5QrcodeScanner(targetId, { fps: 10, qrbox: 250 });
scanner.render(function(decodedText){
try { applyConfig(JSON.parse(decodedText)); scanner.clear(); } catch(e){ toastr.error('QR no contiene JSON válido'); }
});
}
// stop button
document.getElementById(targetId + '-stop').onclick = function () {
if (html5Qr && html5Qr.getState && html5Qr.getState() === Html5Qrcode.ScanStatus.SCANNING) {
html5Qr.stop().catch(function(){});
}
scanDiv.innerHTML = '';
};
} catch (e) {
toastr.error('Error al iniciar escáner: ' + (e && e.message ? e.message : e));
}
} else {
scanDiv.innerHTML = '<p>Escáner no disponible. Copia/pega el JSON o sube un archivo.</p>';
}
};
document.getElementById(btn_save).onclick = () => {
var url = document.getElementById(field_couch).value.trim();
var dbname = document.getElementById(field_couch_dbname).value.trim();
var user = document.getElementById(field_couch_user).value.trim();
var pass = document.getElementById(field_couch_pass).value;
var secret = document.getElementById(field_secret).value || '';
localStorage.setItem('TELESEC_COUCH_URL', "https://" + url);
localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
localStorage.setItem('TELESEC_COUCH_USER', user);
localStorage.setItem('TELESEC_COUCH_PASS', pass);
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
SECRET = secret.toUpperCase();
try {
DB.init({ secret: SECRET, remoteServer: "https://" + url, username: user, password: pass, dbname: dbname || undefined });
toastr.success('Iniciando sincronización con CouchDB');
location.hash = "#login";
toastr.success('Configuración aplicada e iniciando sincronización');
location.hash = '#login';
setTimeout(function () {
location.reload();
} catch (e) {
toastr.error('Error al iniciar sincronización: ' + e.message);
}
};
},
index: function (mid) {
// Check if onboarding is needed
var onboardingComplete = localStorage.getItem('TELESEC_ONBOARDING_COMPLETE');
var hasPersonas = Object.keys(SC_Personas).length > 0;
// If no personas exist and onboarding not complete, redirect to onboarding
if (!hasPersonas && !onboardingComplete && !AC_BYPASS) {
open_page('login,onboarding-config');
setUrlHash('login,onboarding-config');
return;
}
var field_persona = safeuuid();
var btn_guardar = safeuuid();
var btn_reload = safeuuid();
var div_actions = safeuuid();
container.innerHTML = `
<h1>Iniciar sesión</h1>
<fieldset>
<legend>Valores</legend>
<input type="hidden" id="${field_persona}">
<div id="${div_actions}"></div>
<button class="btn5" id="${btn_guardar}">Acceder</button>
<button class="btn3" id="${btn_reload}">Recargar lista</button>
<a class="button btn1" href="#login,setup">Configurar base de datos</a>
</fieldset>
`;
var divact = document.getElementById(div_actions);
addCategory_Personas(
divact,
SC_Personas,
"",
(value) => {
document.getElementById(field_persona).value = value;
},
"¿Quién eres?",
true,
"- Pulsa recargar o rellena los credenciales abajo, si quieres crear un nuevo grupo, pulsa el boton 'Desde cero' -"
);
document.getElementById(btn_guardar).onclick = () => {
if (document.getElementById(field_persona).value == "") {
alert("Tienes que elegir tu cuenta!");
return;
}
SUB_LOGGED_IN_ID = document.getElementById(field_persona).value
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID]
SUB_LOGGED_IN = true
SetPages()
if (location.hash.replace("#", "").startsWith("login")) {
open_page("index");
setUrlHash("index")
} else{
open_page(location.hash.replace("#", ""));
}
};
document.getElementById(btn_reload).onclick = () => {
open_page("login")
};
// AC_BYPASS: allow creating a local persona from the login screen
if (AC_BYPASS) {
var btn_bypass_create = safeuuid();
divact.innerHTML += `<button id="${btn_bypass_create}" class="btn2" style="margin-left:10px;">Crear persona local (bypass)</button>`;
document.getElementById(btn_bypass_create).onclick = () => {
var name = prompt("Nombre de la persona (ej: Admin):");
if (!name) return;
var id = 'bypass-' + Date.now();
var persona = { Nombre: name, Roles: 'ADMIN,' };
DB.put('personas', id, persona).then(() => {
toastr.success('Persona creada: ' + id);
localStorage.setItem('TELESEC_BYPASS_ID', id);
SUB_LOGGED_IN_ID = id;
SUB_LOGGED_IN_DETAILS = persona;
SUB_LOGGED_IN = true;
SetPages();
open_page('index');
}).catch((e) => {
toastr.error('Error creando persona: ' + (e && e.message ? e.message : e));
});
};
}, 400);
} catch (e) {
toastr.error('Error aplicando configuración: ' + (e && e.message ? e.message : e));
}
}
}
document.getElementById(btn_save).onclick = () => {
var url = document.getElementById(field_couch).value.trim();
var secret = document.getElementById(field_secret).value.trim();
var URL_PARSED = parseURL(url);
var host = URL_PARSED.hostname || url;
var user = URL_PARSED.username || '';
var pass = URL_PARSED.password || '';
var dbname = URL_PARSED.pathname ? URL_PARSED.pathname.replace(/^\//, '') : '';
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + host);
localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
localStorage.setItem('TELESEC_COUCH_USER', user);
localStorage.setItem('TELESEC_COUCH_PASS', pass);
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
SECRET = secret.toUpperCase();
try {
DB.init({
secret: SECRET,
remoteServer: 'https://' + host,
username: user,
password: pass,
dbname: dbname || undefined,
});
toastr.success('Iniciando sincronización con CouchDB');
location.hash = '#login';
//location.reload();
} catch (e) {
toastr.error('Error al iniciar sincronización: ' + e.message);
}
};
},
index: function (mid) {
// Check if onboarding is needed
var onboardingComplete = localStorage.getItem('TELESEC_ONBOARDING_COMPLETE');
var hasPersonas = Object.keys(SC_Personas).length > 0;
// If no personas exist and onboarding not complete, redirect to onboarding
if (!hasPersonas && !onboardingComplete && !AC_BYPASS) {
open_page('login,onboarding-config');
setUrlHash('login,onboarding-config');
return;
}
var field_persona = safeuuid();
var btn_guardar = safeuuid();
var btn_reload = safeuuid();
var div_actions = safeuuid();
container.innerHTML = html`
<h1>Iniciar sesión</h1>
<fieldset>
<legend>Valores</legend>
<input type="hidden" id="${field_persona}" />
<div id="${div_actions}"></div>
<button class="btn5" id="${btn_guardar}">Acceder</button>
<button class="btn3" id="${btn_reload}">Recargar lista</button>
<a class="button btn1" href="#login,setup">Configurar base de datos</a>
</fieldset>
`;
var divact = document.getElementById(div_actions);
addCategory_Personas(
divact,
SC_Personas,
'',
(value) => {
document.getElementById(field_persona).value = value;
},
'¿Quién eres?',
true,
"- Pulsa recargar o rellena los credenciales abajo, si quieres crear un nuevo grupo, pulsa el boton 'Desde cero' -"
);
document.getElementById(btn_guardar).onclick = () => {
if (document.getElementById(field_persona).value == '') {
alert('Tienes que elegir tu cuenta!');
return;
}
SUB_LOGGED_IN_ID = document.getElementById(field_persona).value;
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID];
SUB_LOGGED_IN = true;
SetPages();
if (location.hash.replace('#', '').startsWith('login')) {
open_page('index');
setUrlHash('index');
} else {
open_page(location.hash.replace('#', ''));
}
};
document.getElementById(btn_reload).onclick = () => {
open_page('login');
};
// AC_BYPASS: allow creating a local persona from the login screen
if (AC_BYPASS) {
var btn_bypass_create = safeuuid();
divact.innerHTML += `<button id="${btn_bypass_create}" class="btn2" style="margin-left:10px;">Crear persona local (bypass)</button>`;
document.getElementById(btn_bypass_create).onclick = () => {
var name = prompt('Nombre de la persona (ej: Admin):');
if (!name) return;
var id = 'bypass-' + Date.now();
var persona = { Nombre: name, Roles: 'ADMIN,' };
DB.put('personas', id, persona)
.then(() => {
toastr.success('Persona creada: ' + id);
localStorage.setItem('TELESEC_BYPASS_ID', id);
SUB_LOGGED_IN_ID = id;
SUB_LOGGED_IN_DETAILS = persona;
SUB_LOGGED_IN = true;
SetPages();
open_page('index');
})
.catch((e) => {
toastr.error('Error creando persona: ' + (e && e.message ? e.message : e));
});
};
}
},
};

View File

@@ -1,13 +1,13 @@
PERMS["materiales"] = "Almacén";
PERMS["materiales:edit"] = "&gt; Editar";
PERMS['materiales'] = 'Almacén';
PERMS['materiales:edit'] = '&gt; Editar';
PAGES.materiales = {
navcss: "btn2",
icon: "static/appico/shelf.png",
navcss: 'btn2',
icon: 'static/appico/shelf.png',
AccessControl: true,
Title: "Almacén",
Title: 'Almacén',
edit: function (mid) {
if (!checkRole("materiales:edit")) {
setUrlHash("materiales");
if (!checkRole('materiales:edit')) {
setUrlHash('materiales');
return;
}
var nameh1 = safeuuid();
@@ -20,63 +20,68 @@ PAGES.materiales = {
var field_notas = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var FECHA_ISO = new Date().toISOString().split("T")[0];
container.innerHTML = `
var FECHA_ISO = new Date().toISOString().split('T')[0];
container.innerHTML = html`
<h1>Material <code id="${nameh1}"></code></h1>
${BuildQR("materiales," + mid, "Este Material")}
${BuildQR('materiales,' + mid, 'Este Material')}
<fieldset>
<label>
Fecha Revisión<br>
<input type="date" id="${field_revision}"> <a onclick='document.getElementById("${field_revision}").value = "${FECHA_ISO}";'>Hoy - Contado todas las existencias</a><br><br>
Fecha Revisión<br />
<input type="date" id="${field_revision}" />
<a onclick='document.getElementById("${field_revision}").value = "${FECHA_ISO}";'
>Hoy - Contado todas las existencias</a
><br /><br />
</label>
<label>
Nombre<br>
<input type="text" id="${field_nombre}"><br><br>
Nombre<br />
<input type="text" id="${field_nombre}" /><br /><br />
</label>
<label>
Unidad<br>
<input type="text" id="${field_unidad}"><br><br>
Unidad<br />
<input type="text" id="${field_unidad}" /><br /><br />
</label>
<label>
Cantidad Actual<br>
<input type="number" step="0.5" id="${field_cantidad}"><br><br>
Cantidad Actual<br />
<input type="number" step="0.5" id="${field_cantidad}" /><br /><br />
</label>
<label>
Cantidad Minima<br>
<input type="number" step="0.5" id="${field_cantidad_min}"><br><br>
Cantidad Minima<br />
<input type="number" step="0.5" id="${field_cantidad_min}" /><br /><br />
</label>
<label>
Ubicación<br>
<input type="text" id="${field_ubicacion}" value="-"><br><br>
Ubicación<br />
<input type="text" id="${field_ubicacion}" value="-" /><br /><br />
</label>
<label>
Notas<br>
<textarea id="${field_notas}"></textarea><br><br>
</label><hr>
Notas<br />
<textarea id="${field_notas}"></textarea><br /><br />
</label>
<hr />
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
DB.get('materiales', mid).then((data) => {
function load_data(data, ENC = "") {
function load_data(data, ENC = '') {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_nombre).value = data["Nombre"] || "";
document.getElementById(field_unidad).value =
data["Unidad"] || "unidad(es)";
document.getElementById(field_cantidad).value =
data["Cantidad"] || "";
document.getElementById(field_cantidad_min).value =
data["Cantidad_Minima"] || "";
document.getElementById(field_ubicacion).value =
data["Ubicacion"] || "-";
document.getElementById(field_revision).value =
data["Revision"] || "-";
document.getElementById(field_notas).value = data["Notas"] || "";
document.getElementById(field_nombre).value = data['Nombre'] || '';
document.getElementById(field_unidad).value = data['Unidad'] || 'unidad(es)';
document.getElementById(field_cantidad).value = data['Cantidad'] || '';
document.getElementById(field_cantidad_min).value = data['Cantidad_Minima'] || '';
document.getElementById(field_ubicacion).value = data['Ubicacion'] || '-';
document.getElementById(field_revision).value = data['Revision'] || '-';
document.getElementById(field_notas).value = data['Notas'] || '';
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
load_data(data, "%E");
}, 'materiales', mid);
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
load_data(data, '%E');
},
'materiales',
mid
);
} else {
load_data(data || {});
}
@@ -85,10 +90,10 @@ PAGES.materiales = {
// Disable button to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
guardarBtn.disabled = true;
guardarBtn.style.opacity = "0.5";
guardarBtn.style.opacity = '0.5';
var data = {
Nombre: document.getElementById(field_nombre).value,
Unidad: document.getElementById(field_unidad).value,
@@ -98,48 +103,51 @@ PAGES.materiales = {
Revision: document.getElementById(field_revision).value,
Notas: document.getElementById(field_notas).value,
};
document.getElementById("actionStatus").style.display = "block";
DB.put('materiales', mid, data).then(() => {
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("materiales");
}, SAVE_WAIT);
}).catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = "1";
document.getElementById("actionStatus").style.display = "none";
toastr.error("Error al guardar el material");
});
document.getElementById('actionStatus').style.display = 'block';
DB.put('materiales', mid, data)
.then(() => {
toastr.success('Guardado!');
setTimeout(() => {
document.getElementById('actionStatus').style.display = 'none';
setUrlHash('materiales');
}, SAVE_WAIT);
})
.catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = '1';
document.getElementById('actionStatus').style.display = 'none';
toastr.error('Error al guardar el material');
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm("¿Quieres borrar este material?") == true) {
if (confirm('¿Quieres borrar este material?') == true) {
DB.del('materiales', mid).then(() => {
toastr.error("Borrado!");
toastr.error('Borrado!');
setTimeout(() => {
setUrlHash("materiales");
setUrlHash('materiales');
}, SAVE_WAIT);
});
}
};
},
index: function () {
if (!checkRole("materiales")) {
setUrlHash("index");
if (!checkRole('materiales')) {
setUrlHash('index');
return;
}
var btn_new = safeuuid();
var select_ubicacion = safeuuid();
var check_lowstock = safeuuid();
var tableContainer = safeuuid();
container.innerHTML = `
container.innerHTML = html`
<h1>Materiales del Almacén</h1>
<label>
<b>Solo lo que falta:</b>
<input type="checkbox" id="${check_lowstock}" style="height: 25px;width: 25px;">
</label><br>
<label>Filtrar por ubicación:
<input type="checkbox" id="${check_lowstock}" style="height: 25px;width: 25px;" /> </label
><br />
<label
>Filtrar por ubicación:
<select id="${select_ubicacion}">
<option value="">(Todas)</option>
</select>
@@ -149,33 +157,31 @@ PAGES.materiales = {
`;
const config = [
{ key: "Revision", label: "Ult. Revisión", type: "fecha-diff", default: "" },
{ key: "Nombre", label: "Nombre", type: "text", default: "" },
{ key: "Ubicacion", label: "Ubicación", type: "text", default: "--" },
{ key: 'Revision', label: 'Ult. Revisión', type: 'fecha-diff', default: '' },
{ key: 'Nombre', label: 'Nombre', type: 'text', default: '' },
{ key: 'Ubicacion', label: 'Ubicación', type: 'text', default: '--' },
{
key: "Cantidad",
label: "Cantidad",
type: "template",
key: 'Cantidad',
label: 'Cantidad',
type: 'template',
template: (data, element) => {
const min = parseFloat(data.Cantidad_Minima);
const act = parseFloat(data.Cantidad);
const sma = act < min ? `<small>- min. ${data.Cantidad_Minima || "?"}</small>` : ""
element.innerHTML = `${data.Cantidad || "?"} ${
data.Unidad || "?"
} ${sma}`;
const sma = act < min ? `<small>- min. ${data.Cantidad_Minima || '?'}</small>` : '';
element.innerHTML = html`${data.Cantidad || '?'} ${data.Unidad || '?'} ${sma}`;
},
default: "?",
default: '?',
},
{ key: "Notas", label: "Notas", type: "text", default: "" },
{ key: 'Notas', label: 'Notas', type: 'text', default: '' },
];
// Obtener todas las ubicaciones únicas y poblar el <select>, desencriptando si es necesario
DB.map("materiales", (data, key) => {
DB.map('materiales', (data, key) => {
try {
if (!data) return;
function addUbicacion(d) {
const ubicacion = d.Ubicacion || "-";
const ubicacion = d.Ubicacion || '-';
const select = document.getElementById(select_ubicacion);
if (!select) {
@@ -183,50 +189,54 @@ PAGES.materiales = {
return;
}
const optionExists = Array.from(select.options).some(
(opt) => opt.value === ubicacion
);
const optionExists = Array.from(select.options).some((opt) => opt.value === ubicacion);
if (!optionExists) {
const option = document.createElement("option");
const option = document.createElement('option');
option.value = ubicacion;
option.textContent = ubicacion;
select.appendChild(option);
}
}
if (typeof data === "string") {
TS_decrypt(data, SECRET, (dec, wasEncrypted) => {
if (dec && typeof dec === "object") {
addUbicacion(dec);
}
}, 'materiales', key);
if (typeof data === 'string') {
TS_decrypt(
data,
SECRET,
(dec, wasEncrypted) => {
if (dec && typeof dec === 'object') {
addUbicacion(dec);
}
},
'materiales',
key
);
} else {
addUbicacion(data);
}
} catch (error) {
console.warn("Error processing ubicacion:", error);
console.warn('Error processing ubicacion:', error);
}
});
// Función para renderizar la tabla filtrada
function renderTable(filtroUbicacion) {
TS_IndexElement(
"materiales",
'materiales',
config,
"materiales",
'materiales',
document.getElementById(tableContainer),
function (data, new_tr) {
if (parseFloat(data.Cantidad) < parseFloat(data.Cantidad_Minima)) {
new_tr.style.background = "#fcfcb0";
new_tr.style.background = '#fcfcb0';
}
if (parseFloat(data.Cantidad) <= 0) {
new_tr.style.background = "#ffc0c0";
new_tr.style.background = '#ffc0c0';
}
if ((data.Cantidad || "?") == "?") {
new_tr.style.background = "#d0d0ff";
if ((data.Cantidad || '?') == '?') {
new_tr.style.background = '#d0d0ff';
}
if ((data.Revision || "?") == "?") {
new_tr.style.background = "#d0d0ff";
if ((data.Revision || '?') == '?') {
new_tr.style.background = '#d0d0ff';
}
},
function (data) {
@@ -234,16 +244,15 @@ PAGES.materiales = {
!document.getElementById(check_lowstock).checked ||
parseFloat(data.Cantidad) < parseFloat(data.Cantidad_Minima);
var is_region =
filtroUbicacion === "" || data.Ubicacion === filtroUbicacion;
var is_region = filtroUbicacion === '' || data.Ubicacion === filtroUbicacion;
return !(is_low_stock && is_region);
}
);
}
// Inicializar tabla sin filtro
renderTable("");
renderTable('');
// Evento para filtrar por ubicación
document.getElementById(select_ubicacion).onchange = function () {
@@ -254,11 +263,11 @@ PAGES.materiales = {
renderTable(document.getElementById(select_ubicacion).value);
};
if (!checkRole("materiales:edit")) {
document.getElementById(btn_new).style.display = "none";
if (!checkRole('materiales:edit')) {
document.getElementById(btn_new).style.display = 'none';
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash("materiales," + safeuuid(""));
setUrlHash('materiales,' + safeuuid(''));
};
}
},

View File

@@ -1,252 +1,298 @@
PERMS["notas"] = "Notas"
PERMS["notas:edit"] = "&gt; Editar"
PERMS['notas'] = 'Notas';
PERMS['notas:edit'] = '&gt; Editar';
PAGES.notas = {
navcss: "btn5",
icon: "static/appico/edit.png",
AccessControl: true,
Title: "Notas",
edit: function (mid) {
if (!checkRole("notas:edit")) {setUrlHash("notas");return}
var nameh1 = safeuuid();
var field_asunto = safeuuid();
var field_contenido = safeuuid();
var field_autor = safeuuid();
var field_files = safeuuid();
var attachments_list = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var div_actions = safeuuid();
container.innerHTML = `
<h1>Nota <code id="${nameh1}"></code></h1>
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
<legend>Valores</legend>
<div style="max-width: 400px;">
<label>
Asunto<br>
<input type="text" id="${field_asunto}" value=""><br><br>
</label>
<input type="hidden" id="${field_autor}" value="">
<div id="${div_actions}"></div>
</div>
<label>
Contenido<br>
<textarea id="${field_contenido}" style="width: calc(100% - 15px); height: 400px;"></textarea><br><br>
</label>
navcss: 'btn5',
icon: 'static/appico/edit.png',
AccessControl: true,
Title: 'Notas',
edit: function (mid) {
if (!checkRole('notas:edit')) {
setUrlHash('notas');
return;
}
var nameh1 = safeuuid();
var field_asunto = safeuuid();
var field_contenido = safeuuid();
var field_autor = safeuuid();
var field_files = safeuuid();
var attachments_list = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var div_actions = safeuuid();
container.innerHTML = html`
<h1>Nota <code id="${nameh1}"></code></h1>
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
<legend>Valores</legend>
<div style="max-width: 400px;">
<label>
Adjuntos (Fotos o archivos)<br>
<input type="file" id="${field_files}" multiple><br><br>
<div id="${attachments_list}"></div>
Asunto<br />
<input type="text" id="${field_asunto}" value="" /><br /><br />
</label>
<hr>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
var divact = document.getElementById(div_actions);
addCategory_Personas(
divact,
SC_Personas,
SUB_LOGGED_IN_ID,
(value) => {
document.getElementById(field_autor).value = value;
},
"Autor"
);
DB.get('notas', mid).then((data) => {
function load_data(data, ENC = "") {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_asunto).value = data["Asunto"] || "";
document.getElementById(field_contenido).value =
data["Contenido"] || "";
document.getElementById(field_autor).value = data["Autor"] || SUB_LOGGED_IN_ID || "";
<input type="hidden" id="${field_autor}" value="" />
<div id="${div_actions}"></div>
</div>
<label>
Contenido<br />
<textarea
id="${field_contenido}"
style="width: calc(100% - 15px); height: 400px;"
></textarea
><br /><br />
</label>
<label>
Adjuntos (Fotos o archivos)<br />
<input type="file" id="${field_files}" multiple /><br /><br />
<div id="${attachments_list}"></div>
</label>
<hr />
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
var divact = document.getElementById(div_actions);
addCategory_Personas(
divact,
SC_Personas,
SUB_LOGGED_IN_ID,
(value) => {
document.getElementById(field_autor).value = value;
},
'Autor'
);
DB.get('notas', mid).then((data) => {
function load_data(data, ENC = '') {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_asunto).value = data['Asunto'] || '';
document.getElementById(field_contenido).value = data['Contenido'] || '';
document.getElementById(field_autor).value = data['Autor'] || SUB_LOGGED_IN_ID || '';
// Persona select
divact.innerHTML = "";
addCategory_Personas(
divact,
SC_Personas,
data["Autor"] || SUB_LOGGED_IN_ID || "",
(value) => {
document.getElementById(field_autor).value = value;
},
"Autor"
);
// Mostrar adjuntos existentes (si los hay).
// No confiar en `data._attachments` porque `DB.get` devuelve solo `doc.data`.
const attachContainer = document.getElementById(attachments_list);
attachContainer.innerHTML = "";
// Usar API de DB para listar attachments (no acceder a internals desde la UI)
DB.listAttachments('notas', mid).then((list) => {
// Persona select
divact.innerHTML = '';
addCategory_Personas(
divact,
SC_Personas,
data['Autor'] || SUB_LOGGED_IN_ID || '',
(value) => {
document.getElementById(field_autor).value = value;
},
'Autor'
);
// Mostrar adjuntos existentes (si los hay).
// No confiar en `data._attachments` porque `DB.get` devuelve solo `doc.data`.
const attachContainer = document.getElementById(attachments_list);
attachContainer.innerHTML = '';
// Usar API de DB para listar attachments (no acceder a internals desde la UI)
DB.listAttachments('notas', mid)
.then((list) => {
if (!list || !Array.isArray(list)) return;
list.forEach((att) => {
addAttachmentRow(att.name, att.dataUrl);
});
}).catch((e) => { console.warn('listAttachments error', e); });
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data) => {
load_data(data, "%E");
})
.catch((e) => {
console.warn('listAttachments error', e);
});
} else {
load_data(data || {});
}
});
// gestión de archivos seleccionados antes de guardar
const attachmentsToUpload = [];
function addAttachmentRow(name, url) {
const attachContainer = document.getElementById(attachments_list);
const idRow = safeuuid();
const isImage = url && url.indexOf('data:image') === 0;
const preview = isImage ? `<img src="${url}" height="80" style="margin-right:8px;">` : `<a href="${url}" target="_blank">${name}</a>`;
const html = `
}
if (typeof data == 'string') {
TS_decrypt(data, SECRET, (data) => {
load_data(data, '%E');
});
} else {
load_data(data || {});
}
});
// gestión de archivos seleccionados antes de guardar
const attachmentsToUpload = [];
function addAttachmentRow(name, url) {
const attachContainer = document.getElementById(attachments_list);
const idRow = safeuuid();
const isImage = url && url.indexOf('data:image') === 0;
const preview = isImage
? `<img src="${url}" height="80" style="margin-right:8px;">`
: `<a href="${url}" target="_blank">${name}</a>`;
const html = `
<div id="${idRow}" style="display:flex;align-items:center;margin:6px 0;border:1px solid #ddd;padding:6px;border-radius:6px;">
<div style="flex:1">${preview}<strong style="margin-left:8px">${name}</strong></div>
<div><button type="button" class="rojo" data-name="${name}">Borrar</button></div>
</div>`;
attachContainer.insertAdjacentHTML('beforeend', html);
attachContainer.querySelectorAll(`button[data-name="${name}"]`).forEach((btn) => {
btn.onclick = () => {
if (!confirm('¿Borrar este adjunto?')) return;
// Usar API pública en DB para borrar metadata del attachment
DB.deleteAttachment('notas', mid, name).then((ok) => {
attachContainer.insertAdjacentHTML('beforeend', html);
attachContainer.querySelectorAll(`button[data-name="${name}"]`).forEach((btn) => {
btn.onclick = () => {
if (!confirm('¿Borrar este adjunto?')) return;
// Usar API pública en DB para borrar metadata del attachment
DB.deleteAttachment('notas', mid, name)
.then((ok) => {
if (ok) {
document.getElementById(idRow).remove();
toastr.error('Adjunto borrado');
} else {
toastr.error('No se pudo borrar el adjunto');
}
}).catch((e) => { console.warn('deleteAttachment error', e); toastr.error('Error borrando adjunto'); });
};
});
}
document.getElementById(field_files).addEventListener('change', function (e) {
const files = Array.from(e.target.files || []);
files.forEach((file) => {
const reader = new FileReader();
reader.onload = function (ev) {
const dataUrl = ev.target.result;
attachmentsToUpload.push({ name: file.name, data: dataUrl, type: file.type || 'application/octet-stream' });
// mostrar preview temporal
addAttachmentRow(file.name, dataUrl);
};
reader.readAsDataURL(file);
});
// limpiar input para permitir re-subidas del mismo archivo
e.target.value = '';
});
document.getElementById(btn_guardar).onclick = () => {
// Disable button to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
guardarBtn.disabled = true;
guardarBtn.style.opacity = "0.5";
var data = {
Autor: document.getElementById(field_autor).value,
Contenido: document.getElementById(field_contenido).value,
Asunto: document.getElementById(field_asunto).value,
})
.catch((e) => {
console.warn('deleteAttachment error', e);
toastr.error('Error borrando adjunto');
});
};
document.getElementById("actionStatus").style.display = "block";
DB.put('notas', mid, data).then(() => {
});
}
document.getElementById(field_files).addEventListener('change', function (e) {
const files = Array.from(e.target.files || []);
files.forEach((file) => {
const reader = new FileReader();
reader.onload = function (ev) {
const dataUrl = ev.target.result;
attachmentsToUpload.push({
name: file.name,
data: dataUrl,
type: file.type || 'application/octet-stream',
});
// mostrar preview temporal
addAttachmentRow(file.name, dataUrl);
};
reader.readAsDataURL(file);
});
// limpiar input para permitir re-subidas del mismo archivo
e.target.value = '';
});
document.getElementById(btn_guardar).onclick = () => {
// Disable button to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
guardarBtn.disabled = true;
guardarBtn.style.opacity = '0.5';
var data = {
Autor: document.getElementById(field_autor).value,
Contenido: document.getElementById(field_contenido).value,
Asunto: document.getElementById(field_asunto).value,
};
document.getElementById('actionStatus').style.display = 'block';
DB.put('notas', mid, data)
.then(() => {
// subir attachments si los hay
const uploadPromises = [];
attachmentsToUpload.forEach((att) => {
if (DB.putAttachment) {
uploadPromises.push(DB.putAttachment('notas', mid, att.name, att.data, att.type).catch((e) => { console.warn('putAttachment error', e); }));
uploadPromises.push(
DB.putAttachment('notas', mid, att.name, att.data, att.type).catch((e) => {
console.warn('putAttachment error', e);
})
);
}
});
Promise.all(uploadPromises).then(() => {
// limpiar lista temporal y recargar attachments
attachmentsToUpload.length = 0;
try { // recargar lista actual sin salir
const pouchId = 'notas:' + mid;
if (DB && DB._internal && DB._internal.local) {
DB._internal.local.get(pouchId, { attachments: true }).then((doc) => {
const attachContainer = document.getElementById(attachments_list);
attachContainer.innerHTML = '';
if (doc && doc._attachments) {
Object.keys(doc._attachments).forEach((name) => {
try {
const att = doc._attachments[name];
if (att && att.data) {
const durl = 'data:' + (att.content_type || 'application/octet-stream') + ';base64,' + att.data;
addAttachmentRow(name, durl);
return;
}
} catch (e) {}
DB.getAttachment('notas', mid, name).then((durl) => { addAttachmentRow(name, durl); }).catch(() => {});
Promise.all(uploadPromises)
.then(() => {
// limpiar lista temporal y recargar attachments
attachmentsToUpload.length = 0;
try {
// recargar lista actual sin salir
const pouchId = 'notas:' + mid;
if (DB && DB._internal && DB._internal.local) {
DB._internal.local
.get(pouchId, { attachments: true })
.then((doc) => {
const attachContainer = document.getElementById(attachments_list);
attachContainer.innerHTML = '';
if (doc && doc._attachments) {
Object.keys(doc._attachments).forEach((name) => {
try {
const att = doc._attachments[name];
if (att && att.data) {
const durl =
'data:' +
(att.content_type || 'application/octet-stream') +
';base64,' +
att.data;
addAttachmentRow(name, durl);
return;
}
} catch (e) {}
DB.getAttachment('notas', mid, name)
.then((durl) => {
addAttachmentRow(name, durl);
})
.catch(() => {});
});
}
})
.catch(() => {
/* ignore reload errors */
});
}
}).catch(() => { /* ignore reload errors */ });
}
} catch (e) {}
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("notas");
}, SAVE_WAIT);
}).catch((e) => {
console.warn('Attachment upload error', e);
document.getElementById("actionStatus").style.display = "none";
guardarBtn.disabled = false;
guardarBtn.style.opacity = "1";
toastr.error("Error al guardar los adjuntos");
});
}).catch((e) => {
console.warn('DB.put error', e);
document.getElementById("actionStatus").style.display = "none";
}
} catch (e) {}
toastr.success('Guardado!');
setTimeout(() => {
document.getElementById('actionStatus').style.display = 'none';
setUrlHash('notas');
}, SAVE_WAIT);
})
.catch((e) => {
console.warn('Attachment upload error', e);
document.getElementById('actionStatus').style.display = 'none';
guardarBtn.disabled = false;
guardarBtn.style.opacity = '1';
toastr.error('Error al guardar los adjuntos');
});
})
.catch((e) => {
console.warn('DB.put error', e);
document.getElementById('actionStatus').style.display = 'none';
guardarBtn.disabled = false;
guardarBtn.style.opacity = "1";
toastr.error("Error al guardar la nota");
guardarBtn.style.opacity = '1';
toastr.error('Error al guardar la nota');
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm('¿Quieres borrar esta nota?') == true) {
DB.del('notas', mid).then(() => {
toastr.error('Borrado!');
setTimeout(() => {
setUrlHash('notas');
}, SAVE_WAIT);
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm("¿Quieres borrar esta nota?") == true) {
DB.del('notas', mid).then(() => {
toastr.error("Borrado!");
setTimeout(() => {
setUrlHash("notas");
}, SAVE_WAIT);
});
}
};
},
index: function () {
if (!checkRole("notas")) {setUrlHash("index");return}
const tablebody = safeuuid();
var btn_new = safeuuid();
container.innerHTML = `
<h1>Notas</h1>
<button id="${btn_new}">Nueva nota</button>
<div id="cont"></div>
`;
TS_IndexElement(
"notas",
[
{
key: "Autor",
type: "persona-nombre",
default: "",
label: "Autor",
},
{
key: "Asunto",
type: "raw",
default: "",
label: "Asunto",
},
],
"notas",
document.querySelector("#cont"),
);
if (!checkRole("notas:edit")) {
document.getElementById(btn_new).style.display = "none"
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash("notas," + safeuuid(""));
};
}
},
}
};
},
index: function () {
if (!checkRole('notas')) {
setUrlHash('index');
return;
}
const tablebody = safeuuid();
var btn_new = safeuuid();
container.innerHTML = html`
<h1>Notas</h1>
<button id="${btn_new}">Nueva nota</button>
<div id="cont"></div>
`;
TS_IndexElement(
'notas',
[
{
key: 'Autor',
type: 'persona-nombre',
default: '',
label: 'Autor',
},
{
key: 'Asunto',
type: 'raw',
default: '',
label: 'Asunto',
},
],
'notas',
document.querySelector('#cont')
);
if (!checkRole('notas:edit')) {
document.getElementById(btn_new).style.display = 'none';
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash('notas,' + safeuuid(''));
};
}
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
PERMS["personas"] = "Personas";
PERMS["personas:edit"] = "&gt; Editar";
PERMS['personas'] = 'Personas';
PERMS['personas:edit'] = '&gt; Editar';
PAGES.personas = {
navcss: "btn3",
icon: "static/appico/users.png",
navcss: 'btn3',
icon: 'static/appico/users.png',
AccessControl: true,
Title: "Personas",
Title: 'Personas',
edit: function (mid) {
if (!checkRole("personas:edit")) {
setUrlHash("personas");
if (!checkRole('personas:edit')) {
setUrlHash('personas');
return;
}
var nameh1 = safeuuid();
@@ -23,9 +23,9 @@ PAGES.personas = {
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var btn_ver_monedero = safeuuid();
container.innerHTML = `
container.innerHTML = html`
<h1>Persona <code id="${nameh1}"></code></h1>
${BuildQR("personas," + mid, "Esta Persona")}
${BuildQR('personas,' + mid, 'Esta Persona')}
<fieldset>
<label>
Nombre<br>
@@ -88,16 +88,16 @@ PAGES.personas = {
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
var resized = "";
var resized = '';
var pdel = document.getElementById(permisosdet);
DB.get('personas', mid).then((data) => {
function load_data(data, ENC = "") {
function load_data(data, ENC = '') {
document.getElementById(nameh1).innerText = mid;
var pot = "<ul>";
var pot = '<ul>';
Object.entries(PERMS).forEach((page) => {
var c = "";
if ((data["Roles"] || ",").split(",").includes(page[0])) {
c = "checked";
var c = '';
if ((data['Roles'] || ',').split(',').includes(page[0])) {
c = 'checked';
}
pot += `
<li><label>
@@ -106,35 +106,41 @@ PAGES.personas = {
</label></li>
`;
});
pdel.innerHTML = pot + "</ul>";
document.getElementById(field_nombre).value = data["Nombre"] || "";
document.getElementById(field_zona).value = data["Region"] || "";
document.getElementById(field_anilla).value = data["SC_Anilla"] || "";
pdel.innerHTML = pot + '</ul>';
document.getElementById(field_nombre).value = data['Nombre'] || '';
document.getElementById(field_zona).value = data['Region'] || '';
document.getElementById(field_anilla).value = data['SC_Anilla'] || '';
// set fallback image immediately
document.getElementById(render_foto).src = data["Foto"] || "static/ico/user_generic.png";
resized = data["Foto"] || "static/ico/user_generic.png";
document.getElementById(render_foto).src = data['Foto'] || 'static/ico/user_generic.png';
resized = data['Foto'] || 'static/ico/user_generic.png';
// try to load attachment 'foto' if present (preferred storage)
DB.getAttachment('personas', mid, 'foto').then((durl) => {
if (durl) {
document.getElementById(render_foto).src = durl;
resized = durl;
}
}).catch(() => {});
document.getElementById(field_notas).value = data["markdown"] || "";
document.getElementById(field_monedero_balance).value =
data["Monedero_Balance"] || 0;
document.getElementById(field_monedero_notas).value =
data["Monedero_Notas"] || "";
DB.getAttachment('personas', mid, 'foto')
.then((durl) => {
if (durl) {
document.getElementById(render_foto).src = durl;
resized = durl;
}
})
.catch(() => {});
document.getElementById(field_notas).value = data['markdown'] || '';
document.getElementById(field_monedero_balance).value = data['Monedero_Balance'] || 0;
document.getElementById(field_monedero_notas).value = data['Monedero_Notas'] || '';
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
load_data(data, "%E");
}, 'personas', mid);
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
load_data(data, '%E');
},
'personas',
mid
);
} else {
load_data(data || {});
}
});
document.getElementById(field_foto).addEventListener("change", function (e) {
document.getElementById(field_foto).addEventListener('change', function (e) {
const file = e.target.files[0];
if (!file) return;
// Do NOT resize — keep original uploaded image
@@ -150,76 +156,78 @@ PAGES.personas = {
// Disable button to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
guardarBtn.disabled = true;
guardarBtn.style.opacity = "0.5";
guardarBtn.style.opacity = '0.5';
var dt = new FormData(pdel);
var data = {
Nombre: document.getElementById(field_nombre).value,
Region: document.getElementById(field_zona).value,
Roles: dt.getAll("perm").join(",") + ",",
Roles: dt.getAll('perm').join(',') + ',',
SC_Anilla: document.getElementById(field_anilla).value,
// Foto moved to PouchDB attachment named 'foto'
markdown: document.getElementById(field_notas).value,
Monedero_Balance:
parseFloat(document.getElementById(field_monedero_balance).value) ||
0,
Monedero_Balance: parseFloat(document.getElementById(field_monedero_balance).value) || 0,
Monedero_Notas: document.getElementById(field_monedero_notas).value,
};
document.getElementById("actionStatus").style.display = "block";
DB.put('personas', mid, data).then(() => {
// if resized is a data URL (new/updated image), save as attachment
var attachPromise = Promise.resolve(true);
if (typeof resized === 'string' && resized.indexOf('data:') === 0) {
attachPromise = DB.putAttachment('personas', mid, 'foto', resized, 'image/png');
}
attachPromise.then(() => {
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("personas");
}, SAVE_WAIT);
}).catch((e) => {
console.warn('putAttachment error', e);
document.getElementById("actionStatus").style.display = "none";
document.getElementById('actionStatus').style.display = 'block';
DB.put('personas', mid, data)
.then(() => {
// if resized is a data URL (new/updated image), save as attachment
var attachPromise = Promise.resolve(true);
if (typeof resized === 'string' && resized.indexOf('data:') === 0) {
attachPromise = DB.putAttachment('personas', mid, 'foto', resized, 'image/png');
}
attachPromise
.then(() => {
toastr.success('Guardado!');
setTimeout(() => {
document.getElementById('actionStatus').style.display = 'none';
setUrlHash('personas');
}, SAVE_WAIT);
})
.catch((e) => {
console.warn('putAttachment error', e);
document.getElementById('actionStatus').style.display = 'none';
guardarBtn.disabled = false;
guardarBtn.style.opacity = '1';
toastr.error('Error al guardar la foto');
});
})
.catch((e) => {
console.warn('DB.put error', e);
document.getElementById('actionStatus').style.display = 'none';
guardarBtn.disabled = false;
guardarBtn.style.opacity = "1";
toastr.error("Error al guardar la foto");
guardarBtn.style.opacity = '1';
toastr.error('Error al guardar la persona');
});
}).catch((e) => {
console.warn('DB.put error', e);
document.getElementById("actionStatus").style.display = "none";
guardarBtn.disabled = false;
guardarBtn.style.opacity = "1";
toastr.error("Error al guardar la persona");
});
};
document.getElementById(btn_ver_monedero).onclick = () => {
setUrlHash("pagos"); // Navigate to pagos and show transactions for this person
setUrlHash('pagos'); // Navigate to pagos and show transactions for this person
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm("¿Quieres borrar esta persona?") == true) {
if (confirm('¿Quieres borrar esta persona?') == true) {
DB.del('personas', mid).then(() => {
toastr.error("Borrado!");
toastr.error('Borrado!');
setTimeout(() => {
setUrlHash("personas");
setUrlHash('personas');
}, SAVE_WAIT);
});
}
};
},
index: function () {
if (!checkRole("personas")) {
setUrlHash("index");
if (!checkRole('personas')) {
setUrlHash('index');
return;
}
var btn_new = safeuuid();
container.innerHTML = `
container.innerHTML = html`
<h1>Personas</h1>
<button id="${btn_new}">Nueva Persona</button>
<div id="tableContainer"></div>
`;
`;
const config = [
// {
@@ -227,28 +235,28 @@ PAGES.personas = {
// type: "persona",
// self: true,
// },
{ key: "Foto", label: "Foto", type: "attachment-persona", default: "", self: true },
{ key: "Nombre", label: "Nombre", type: "text", default: "" },
{ key: "Region", label: "Zona", type: "text", default: "" },
{ key: "Monedero_Balance", label: "Saldo Monedero", type: "moneda" },
{ key: 'Foto', label: 'Foto', type: 'attachment-persona', default: '', self: true },
{ key: 'Nombre', label: 'Nombre', type: 'text', default: '' },
{ key: 'Region', label: 'Zona', type: 'text', default: '' },
{ key: 'Monedero_Balance', label: 'Saldo Monedero', type: 'moneda' },
//{ key: "markdown", label: "Notas", type: "markdown", default: "" },
//{ key: "Roles", label: "Permisos", type: "text", default: "" }
];
TS_IndexElement(
"personas",
'personas',
config,
"personas",
document.getElementById("tableContainer"),
'personas',
document.getElementById('tableContainer'),
undefined,
undefined,
true // Enable global search bar
);
if (!checkRole("personas:edit")) {
document.getElementById(btn_new).style.display = "none";
if (!checkRole('personas:edit')) {
document.getElementById(btn_new).style.display = 'none';
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash("personas," + safeuuid(""));
setUrlHash('personas,' + safeuuid(''));
};
}
},

View File

@@ -1,41 +1,67 @@
PERMS["resumen_diario"] = "Resumen diario (Solo docentes!)";
PERMS['resumen_diario'] = 'Resumen diario (Solo docentes!)';
PAGES.resumen_diario = {
icon: "static/appico/calendar.png",
navcss: "btn3",
icon: 'static/appico/calendar.png',
navcss: 'btn3',
AccessControl: true,
Title: "Resumen Diario",
Title: 'Resumen Diario',
index: function () {
var data_Comedor = safeuuid();
var data_Tareas = safeuuid();
var data_Diario = safeuuid();
var data_Weather = safeuuid();
if (!checkRole("resumen_diario")) {
setUrlHash("index");
if (!checkRole('resumen_diario')) {
setUrlHash('index');
return;
}
container.innerHTML = `
container.innerHTML = html`
<h1>Resumen Diario ${CurrentISODate()}</h1>
<button onclick="print()">Imprimir</button>
<br><span class="btn7" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"><b>Menú Comedor:</b> <br><span id="${data_Comedor}">Cargando...</span></span>
<br><span class="btn6" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"><b>Tareas:</b> <br><pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Tareas}">Cargando...</pre></span>
<br><span class="btn5" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"><b>Diario:</b> <br><pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Diario}">Cargando...</pre></span>
<br><span class="btn4" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"><b>Clima:</b> <br><img loading="lazy" style="padding: 15px; background-color: white; height: 75px;" id="${data_Weather}"></span>
<br /><span
class="btn7"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"
><b>Menú Comedor:</b> <br /><span id="${data_Comedor}">Cargando...</span></span
>
<br /><span
class="btn6"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"
><b>Tareas:</b> <br />
<pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Tareas}">
Cargando...</pre
>
</span>
<br /><span
class="btn5"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"
><b>Diario:</b> <br />
<pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Diario}">
Cargando...</pre
>
</span>
<br /><span
class="btn4"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"
><b>Clima:</b> <br /><img
loading="lazy"
style="padding: 15px; background-color: white; height: 75px;"
id="${data_Weather}"
/></span>
`;
//#region Cargar Clima
// Get location from DB settings.weather_location; if missing ask user and save it
// url format: https://wttr.in/<loc>?F0m
DB.get('settings','weather_location').then((loc) => {
DB.get('settings', 'weather_location').then((loc) => {
if (!loc) {
loc = prompt("Introduce tu ubicación para el clima (ciudad, país):", "Madrid, Spain");
loc = prompt('Introduce tu ubicación para el clima (ciudad, país):', 'Madrid, Spain');
if (loc) {
DB.put('settings','weather_location', loc);
DB.put('settings', 'weather_location', loc);
}
}
if (loc) {
document.getElementById(data_Weather).src = "https://wttr.in/" + encodeURIComponent(loc) + "_IF0m_background=FFFFFF.png";
document.getElementById(data_Weather).src =
'https://wttr.in/' + encodeURIComponent(loc) + '_IF0m_background=FFFFFF.png';
} else {
document.getElementById(data_Weather).src = "https://wttr.in/_IF0m_background=FFFFFF.png";
document.getElementById(data_Weather).src = 'https://wttr.in/_IF0m_background=FFFFFF.png';
}
});
//#endregion Cargar Clima
@@ -43,17 +69,20 @@ PAGES.resumen_diario = {
DB.get('comedor', CurrentISODate()).then((data) => {
function add_row(data) {
// Fix newlines
data.Platos = data.Platos || "No hay platos registrados para hoy.";
data.Platos = data.Platos || 'No hay platos registrados para hoy.';
// Display platos
document.getElementById(data_Comedor).innerHTML = data.Platos.replace(
/\n/g,
"<br>"
);
document.getElementById(data_Comedor).innerHTML = data.Platos.replace(/\n/g, '<br>');
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
add_row(data || {});
}, 'comedor', CurrentISODate());
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'comedor',
CurrentISODate()
);
} else {
add_row(data || {});
}
@@ -63,17 +92,20 @@ PAGES.resumen_diario = {
DB.get('notas', 'tareas').then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || "No hay tareas.";
data.Contenido = data.Contenido || 'No hay tareas.';
// Display platos
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(
/\n/g,
"<br>"
);
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(/\n/g, '<br>');
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
add_row(data || {});
}, 'notas', 'tareas');
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'notas',
'tareas'
);
} else {
add_row(data || {});
}
@@ -83,17 +115,20 @@ PAGES.resumen_diario = {
DB.get('aulas_informes', 'diario-' + CurrentISODate()).then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || "No hay un diario.";
data.Contenido = data.Contenido || 'No hay un diario.';
// Display platos
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(
/\n/g,
"<br>"
);
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(/\n/g, '<br>');
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
add_row(data || {});
}, 'aulas_informes', 'diario-' + CurrentISODate());
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'aulas_informes',
'diario-' + CurrentISODate()
);
} else {
add_row(data || {});
}

View File

@@ -1,13 +1,13 @@
PERMS["supercafe"] = "Cafetería";
PERMS["supercafe:edit"] = "&gt; Editar";
PERMS['supercafe'] = 'Cafetería';
PERMS['supercafe:edit'] = '&gt; Editar';
PAGES.supercafe = {
navcss: "btn4",
icon: "static/appico/cup.png",
navcss: 'btn4',
icon: 'static/appico/cup.png',
AccessControl: true,
Title: "Cafetería",
Title: 'Cafetería',
edit: function (mid) {
if (!checkRole("supercafe:edit")) {
setUrlHash("supercafe");
if (!checkRole('supercafe:edit')) {
setUrlHash('supercafe');
return;
}
var nameh1 = safeuuid();
@@ -19,46 +19,46 @@ PAGES.supercafe = {
var div_actions = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
container.innerHTML = `
container.innerHTML = html`
<h1>Comanda <code id="${nameh1}"></code></h1>
<button onclick="setUrlHash('supercafe');">Salir</button>
<fieldset style="text-align: center;">
<legend>Rellenar comanda</legend>
<label style="display: none;">
Fecha<br>
<input readonly disabled type="text" id="${field_fecha}" value=""><br><br>
</label>
<label style="display: none;">
Persona<br>
<input type="hidden" id="${field_persona}">
<br><br>
</label>
<label style="display: none;">
Comanda (utiliza el panel de relleno)<br>
<textarea readonly disabled id="${field_comanda}"></textarea><br><br>
</label>
<div id="${div_actions}" open>
<!--<summary>Mostrar botones de relleno</summary>-->
</div>
<label>
Notas<br>
<textarea id="${field_notas}"></textarea><br><br>
</label>
<label style="display: none;">
Estado<br>
<input readonly disabled type="text" id="${field_estado}" value="%%">
<br>Modificar en el listado de comandas<br>
</label>
<button id=${btn_guardar} class="btn5">Guardar</button>
<button id=${btn_borrar} class="rojo">Borrar</button>
<legend>Rellenar comanda</legend>
<label style="display: none;">
Fecha<br />
<input readonly disabled type="text" id="${field_fecha}" value="" /><br /><br />
</label>
<label style="display: none;">
Persona<br />
<input type="hidden" id="${field_persona}" />
<br /><br />
</label>
<label style="display: none;">
Comanda (utiliza el panel de relleno)<br />
<textarea readonly disabled id="${field_comanda}"></textarea><br /><br />
</label>
<div id="${div_actions}" open>
<!--<summary>Mostrar botones de relleno</summary>-->
</div>
<label>
Notas<br />
<textarea id="${field_notas}"></textarea><br /><br />
</label>
<label style="display: none;">
Estado<br />
<input readonly disabled type="text" id="${field_estado}" value="%%" />
<br />Modificar en el listado de comandas<br />
</label>
<button id=${btn_guardar} class="btn5">Guardar</button>
<button id=${btn_borrar} class="rojo">Borrar</button>
</fieldset>
`;
`;
var currentData = {};
var currentPersonaID = "";
var currentPersonaID = '';
var divact = document.getElementById(div_actions);
function loadActions() {
divact.innerHTML = "";
divact.innerHTML = '';
addCategory_Personas(divact, SC_Personas, currentPersonaID, (value) => {
document.getElementById(field_persona).value = value;
});
@@ -77,23 +77,29 @@ PAGES.supercafe = {
}
loadActions();
DB.get('supercafe', mid).then((data) => {
function load_data(data, ENC = "") {
function load_data(data, ENC = '') {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_fecha).value = data["Fecha"] || CurrentISODate();
document.getElementById(field_persona).value = data["Persona"] || "";
currentPersonaID = data["Persona"] || "";
document.getElementById(field_fecha).value = data['Fecha'] || CurrentISODate();
document.getElementById(field_persona).value = data['Persona'] || '';
currentPersonaID = data['Persona'] || '';
document.getElementById(field_comanda).value =
SC_parse(JSON.parse(data["Comanda"] || "{}")) || "";
document.getElementById(field_notas).value = data["Notas"] || "";
document.getElementById(field_estado).value = data["Estado"] || "%%";
currentData = JSON.parse(data["Comanda"] || "{}");
SC_parse(JSON.parse(data['Comanda'] || '{}')) || '';
document.getElementById(field_notas).value = data['Notas'] || '';
document.getElementById(field_estado).value = data['Estado'] || '%%';
currentData = JSON.parse(data['Comanda'] || '{}');
loadActions();
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
load_data(data, "%E");
}, 'supercafe', mid);
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
load_data(data, '%E');
},
'supercafe',
mid
);
} else {
load_data(data || {});
}
@@ -102,69 +108,69 @@ PAGES.supercafe = {
// Check if button is already disabled to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
// Validate before disabling button
if (document.getElementById(field_persona).value == "") {
alert("¡Hay que elegir una persona!");
if (document.getElementById(field_persona).value == '') {
alert('¡Hay que elegir una persona!');
return;
}
// Disable button after validation passes
guardarBtn.disabled = true;
guardarBtn.style.opacity = "0.5";
guardarBtn.style.opacity = '0.5';
var data = {
Fecha: document.getElementById(field_fecha).value,
Persona: document.getElementById(field_persona).value,
Comanda: JSON.stringify(currentData),
Notas: document.getElementById(field_notas).value,
Estado: document
.getElementById(field_estado)
.value.replace("%%", "Pedido"),
Estado: document.getElementById(field_estado).value.replace('%%', 'Pedido'),
};
document.getElementById("actionStatus").style.display = "block";
DB.put('supercafe', mid, data).then(() => {
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("supercafe");
}, SAVE_WAIT);
}).catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = "1";
document.getElementById("actionStatus").style.display = "none";
toastr.error("Error al guardar la comanda");
});
document.getElementById('actionStatus').style.display = 'block';
DB.put('supercafe', mid, data)
.then(() => {
toastr.success('Guardado!');
setTimeout(() => {
document.getElementById('actionStatus').style.display = 'none';
setUrlHash('supercafe');
}, SAVE_WAIT);
})
.catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = '1';
document.getElementById('actionStatus').style.display = 'none';
toastr.error('Error al guardar la comanda');
});
};
document.getElementById(btn_borrar).onclick = () => {
if (
confirm(
"¿Quieres borrar esta comanda? - NO se actualizará el monedero de la persona asignada."
'¿Quieres borrar esta comanda? - NO se actualizará el monedero de la persona asignada.'
) == true
) {
DB.del('supercafe', mid).then(() => {
setTimeout(() => {
setUrlHash("supercafe");
setUrlHash('supercafe');
}, SAVE_WAIT);
});
}
};
},
index: function () {
if (!checkRole("supercafe")) {
setUrlHash("index");
if (!checkRole('supercafe')) {
setUrlHash('index');
return;
}
var tts = false;
var sc_nobtn = "";
if (urlParams.get("sc_nobtn") == "yes") {
sc_nobtn = "pointer-events: none; opacity: 0.5";
var sc_nobtn = '';
if (urlParams.get('sc_nobtn') == 'yes') {
sc_nobtn = 'pointer-events: none; opacity: 0.5';
}
var ev = setTimeout(() => {
tts = true;
console.log("TTS Enabled");
toastr.info("Texto a voz disponible");
console.log('TTS Enabled');
toastr.info('Texto a voz disponible');
}, 6500);
EventListeners.Timeout.push(ev);
const tablebody = safeuuid();
@@ -173,58 +179,64 @@ PAGES.supercafe = {
var totalprecio = safeuuid();
var tts_check = safeuuid();
var old = {};
container.innerHTML = `
container.innerHTML = html`
<h1>Cafetería - Total: <span id="${totalprecio}">0</span>c</h1>
<button id="${btn_new}" style="${sc_nobtn};">Nueva comanda</button>
<br>
<br />
<label>
<b>Habilitar avisos:</b>
<input type="checkbox" id="${tts_check}" style="height: 25px;width: 25px;">
<input type="checkbox" id="${tts_check}" style="height: 25px;width: 25px;" />
</label>
<details style="background: beige; padding: 15px; border-radius: 15px; border: 2px solid black" open>
<details
style="background: beige; padding: 15px; border-radius: 15px; border: 2px solid black"
open
>
<summary>Todas las comandas</summary>
<div id="cont1"></div>
</details>
<br>
<details style="background: lightpink; padding: 15px; border-radius: 15px; border: 2px solid black" open>
<br />
<details
style="background: lightpink; padding: 15px; border-radius: 15px; border: 2px solid black"
open
>
<summary>Deudas</summary>
<div id="cont2"></div>
</details>
`;
`;
var config = [
{
key: "Persona",
type: "persona",
default: "",
label: "Persona",
key: 'Persona',
type: 'persona',
default: '',
label: 'Persona',
},
{
key: "Estado",
type: "comanda-status",
default: "",
label: "Estado",
key: 'Estado',
type: 'comanda-status',
default: '',
label: 'Estado',
},
{
key: "Comanda",
type: "comanda",
default: "",
label: "Comanda",
key: 'Comanda',
type: 'comanda',
default: '',
label: 'Comanda',
},
];
if (!checkRole("supercafe:edit")) {
if (!checkRole('supercafe:edit')) {
config = [
{
key: "Persona",
type: "persona",
default: "",
label: "Persona",
key: 'Persona',
type: 'persona',
default: '',
label: 'Persona',
},
{
key: "Comanda",
type: "comanda",
default: "",
label: "Comanda",
key: 'Comanda',
type: 'comanda',
default: '',
label: 'Comanda',
},
];
}
@@ -239,42 +251,42 @@ PAGES.supercafe = {
return tot;
}
TS_IndexElement(
"supercafe",
'supercafe',
config,
"supercafe",
document.querySelector("#cont1"),
'supercafe',
document.querySelector('#cont1'),
(data, new_tr) => {
// new_tr.style.backgroundColor = "#FFCCCB";
comandasTot[data._key] = SC_priceCalc(JSON.parse(data.Comanda))[0];
calcPrecio();
if (data.Estado == "Pedido") {
new_tr.style.backgroundColor = "#FFFFFF";
if (data.Estado == 'Pedido') {
new_tr.style.backgroundColor = '#FFFFFF';
}
if (data.Estado == "En preparación") {
new_tr.style.backgroundColor = "#FFCCCB";
if (data.Estado == 'En preparación') {
new_tr.style.backgroundColor = '#FFCCCB';
}
if (data.Estado == "Listo") {
new_tr.style.backgroundColor = "gold";
if (data.Estado == 'Listo') {
new_tr.style.backgroundColor = 'gold';
}
if (data.Estado == "Entregado") {
new_tr.style.backgroundColor = "lightgreen";
if (data.Estado == 'Entregado') {
new_tr.style.backgroundColor = 'lightgreen';
}
if (data.Estado == "Deuda") {
new_tr.style.backgroundColor = "#f5d3ff";
if (data.Estado == 'Deuda') {
new_tr.style.backgroundColor = '#f5d3ff';
}
},
(data) => {
if (data.Estado == "Deuda") {
if (data.Estado == 'Deuda') {
return true;
}
var key = data._key;
if (old[key] == undefined) {
old[key] = "";
old[key] = '';
}
if (old[key] != data.Estado) {
if (tts && document.getElementById(tts_check).checked) {
var msg = `Comanda de ${SC_Personas[data.Persona].Region}. - ${
JSON.parse(data.Comanda)["Selección"]
JSON.parse(data.Comanda)['Selección']
}. - ${SC_Personas[data.Persona].Nombre}. - ${data.Estado}`;
let utterance = new SpeechSynthesisUtterance(msg);
utterance.rate = 0.9;
@@ -288,43 +300,43 @@ PAGES.supercafe = {
//Deudas
TS_IndexElement(
"supercafe",
'supercafe',
config,
"supercafe",
document.querySelector("#cont2"),
'supercafe',
document.querySelector('#cont2'),
(data, new_tr) => {
// new_tr.style.backgroundColor = "#FFCCCB";
comandasTot[data._key] = 0; // No mostrar comandas en deuda.
calcPrecio();
if (data.Estado == "Pedido") {
new_tr.style.backgroundColor = "#FFFFFF";
if (data.Estado == 'Pedido') {
new_tr.style.backgroundColor = '#FFFFFF';
}
if (data.Estado == "En preparación") {
new_tr.style.backgroundColor = "#FFCCCB";
if (data.Estado == 'En preparación') {
new_tr.style.backgroundColor = '#FFCCCB';
}
if (data.Estado == "Listo") {
new_tr.style.backgroundColor = "gold";
if (data.Estado == 'Listo') {
new_tr.style.backgroundColor = 'gold';
}
if (data.Estado == "Entregado") {
new_tr.style.backgroundColor = "lightgreen";
if (data.Estado == 'Entregado') {
new_tr.style.backgroundColor = 'lightgreen';
}
if (data.Estado == "Deuda") {
new_tr.style.backgroundColor = "#f5d3ff";
if (data.Estado == 'Deuda') {
new_tr.style.backgroundColor = '#f5d3ff';
}
},
(data) => {
if (data.Estado != "Deuda") {
if (data.Estado != 'Deuda') {
return true;
}
var key = data._key;
if (old[key] == undefined) {
old[key] = "";
old[key] = '';
}
if (old[key] != data.Estado) {
if (tts && document.getElementById(tts_check).checked) {
var msg = `Comanda de ${SC_Personas[data.Persona].Region}. - ${
JSON.parse(data.Comanda)["Selección"]
JSON.parse(data.Comanda)['Selección']
}. - ${SC_Personas[data.Persona].Nombre}. - ${data.Estado}`;
let utterance = new SpeechSynthesisUtterance(msg);
utterance.rate = 0.9;
@@ -335,11 +347,11 @@ PAGES.supercafe = {
old[key] = data.Estado;
}
);
if (!checkRole("supercafe:edit")) {
document.getElementById(btn_new).style.display = "none";
if (!checkRole('supercafe:edit')) {
document.getElementById(btn_new).style.display = 'none';
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash("supercafe," + safeuuid(""));
setUrlHash('supercafe,' + safeuuid(''));
};
}
},

View File

@@ -1,28 +1,28 @@
let newWorker;
function showUpdateBar() {
let snackbar = document.getElementById("snackbar");
snackbar.className = "show";
let snackbar = document.getElementById('snackbar');
snackbar.className = 'show';
}
// The click event on the pop up notification
document.getElementById("reload").addEventListener("click", function () {
document.getElementById('reload').addEventListener('click', function () {
setTimeout(() => {
removeCache();
}, 1000);
newWorker.postMessage({ action: "skipWaiting" });
newWorker.postMessage({ action: 'skipWaiting' });
});
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js").then((reg) => {
reg.addEventListener("updatefound", () => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js').then((reg) => {
reg.addEventListener('updatefound', () => {
// A wild service worker has appeared in reg.installing!
newWorker = reg.installing;
newWorker.addEventListener("statechange", () => {
newWorker.addEventListener('statechange', () => {
// Has network.state changed?
switch (newWorker.state) {
case "installed":
case 'installed':
if (navigator.serviceWorker.controller) {
// new update available
showUpdateBar();
@@ -35,7 +35,7 @@ if ("serviceWorker" in navigator) {
});
let refreshing;
navigator.serviceWorker.addEventListener("controllerchange", function () {
navigator.serviceWorker.addEventListener('controllerchange', function () {
if (refreshing) return;
window.location.reload();
refreshing = true;

View File

@@ -1,10 +1,8 @@
var CACHE = "telesec_%%VERSIONCO%%";
importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js"
);
var CACHE = 'telesec_%%VERSIONCO%%';
importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js');
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
@@ -18,9 +16,8 @@ self.addEventListener("message", (event) => {
// All but couchdb
workbox.routing.registerRoute(
({ url }) =>
!url.pathname.startsWith("/_couchdb/") && url.origin === self.location.origin,
({ url }) => !url.pathname.startsWith('/_couchdb/') && url.origin === self.location.origin,
new workbox.strategies.NetworkFirst({
cacheName: CACHE,
})
);
);