Compare commits
56 Commits
copilot/re
...
v2026.03.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d322e5696 | ||
|
|
0138e0ca69 | ||
|
|
3e8542c9de | ||
|
|
90df81d308 | ||
|
|
53941da35c | ||
|
|
105c911c59 | ||
|
|
cb12894455 | ||
|
|
9d808ed63e | ||
|
|
d0593d3d46 | ||
|
|
8b7d0258ae | ||
|
|
9a760a1d24 | ||
|
|
e1f780ea11 | ||
|
|
7ad2e9c142 | ||
|
|
879554a7ab | ||
|
|
0ef6e5a233 | ||
|
|
d905e86bbf | ||
|
|
3764473b5b | ||
|
|
382e31158a | ||
|
|
09a9a95df0 | ||
|
|
b04dbbf19d | ||
|
|
7619444556 | ||
|
|
076aa45337 | ||
|
|
0b1419fae2 | ||
|
|
74afb2a499 | ||
|
|
543d1c3202 | ||
|
|
75947d3468 | ||
|
|
9ab0472e2a | ||
|
|
aa993df2bf | ||
|
|
e0da65811e | ||
|
|
eb6a956cdc | ||
|
|
dc4ba25b20 | ||
|
|
129188c022 | ||
|
|
9d4ce881c6 | ||
|
|
4e1727adc3 | ||
|
|
db5b07bb44 | ||
|
|
61b8cb8af4 | ||
|
|
2ee03aa204 | ||
|
|
8b29e3f425 | ||
|
|
31697f2448 | ||
|
|
468d08110d | ||
|
|
07e2c9a98e | ||
|
|
8b6140929e | ||
|
|
05ea9a9d8b | ||
|
|
75c319c701 | ||
|
|
8a9fee46da | ||
|
|
6d7def5f18 | ||
|
|
ddfd653d68 | ||
|
|
b0160b3b66 | ||
|
|
d6809e51d1 | ||
|
|
0db86f3dd2 | ||
|
|
dd195c5157 | ||
|
|
f472baacf6 | ||
|
|
1e5de2c686 | ||
|
|
bf9ba4ceef | ||
|
|
c2eac955fe | ||
|
|
a02d7956ca |
2
.github/copilot-instructions.md
vendored
@@ -94,8 +94,6 @@ TeleSec is a Spanish Progressive Web Application (PWA) built with vanilla JavaSc
|
|||||||
├── manifest.json # PWA manifest
|
├── manifest.json # PWA manifest
|
||||||
├── *.png, *.jpg # Icons and images
|
├── *.png, *.jpg # Icons and images
|
||||||
├── static/ # JavaScript libraries and CSS
|
├── static/ # JavaScript libraries and CSS
|
||||||
│ │ ├── pouchdb (via CDN) # PouchDB is used for local storage and replication
|
|
||||||
│ ├── webrtc.js # WebRTC functionality
|
|
||||||
│ ├── euskaditech-css/ # CSS framework
|
│ ├── euskaditech-css/ # CSS framework
|
||||||
│ └── ico/ # Application icons
|
│ └── ico/ # Application icons
|
||||||
└── page/ # Page-specific assets (empty placeholder)
|
└── page/ # Page-specific assets (empty placeholder)
|
||||||
|
|||||||
46
.github/workflows/windows-agent-release.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: Build Windows Agent (Release)
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-windows-agent:
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install pyinstaller
|
||||||
|
|
||||||
|
- name: Build hidden EXE with PyInstaller
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cd python_sdk ; pyinstaller --noconfirm --clean --onefile --noconsole --hidden-import=telesec_couchdb --name telesec-windows-agent windows_agent.py
|
||||||
|
|
||||||
|
- name: Upload workflow artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: telesec-windows-agent
|
||||||
|
path: python_sdk/dist/telesec-windows-agent.exe
|
||||||
|
|
||||||
|
- name: Upload asset to GitHub Release
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: python_sdk/dist/telesec-windows-agent.exe
|
||||||
9
.gitignore
vendored
@@ -3,3 +3,12 @@ radata/*
|
|||||||
node_modules/*
|
node_modules/*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
._*
|
._*
|
||||||
|
# Python
|
||||||
|
__pycache__/*
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.egg-info/*
|
||||||
|
*.egg
|
||||||
|
.venv/*
|
||||||
|
venv/*
|
||||||
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"embeddedLanguageFormatting": "auto"
|
||||||
|
}
|
||||||
6
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"tobermory.es6-string-html",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
92
README.md
@@ -1,2 +1,94 @@
|
|||||||
# TeleSec
|
# TeleSec
|
||||||
Nuevo programa de datos
|
Nuevo programa de datos
|
||||||
|
|
||||||
|
## Python SDK (CouchDB directo)
|
||||||
|
|
||||||
|
Se añadió un SDK Python en `python_sdk/` para acceder directamente a CouchDB (sin replicación local), compatible con el formato de cifrado de `TS_encrypt`:
|
||||||
|
|
||||||
|
- Formato: `RSA{...}`
|
||||||
|
- Algoritmo: `CryptoJS.AES.encrypt(payload, secret)` (modo passphrase/OpenSSL)
|
||||||
|
|
||||||
|
### Instalación
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Uso rápido
|
||||||
|
|
||||||
|
```python
|
||||||
|
from python_sdk import TeleSecCouchDB
|
||||||
|
|
||||||
|
db = TeleSecCouchDB(
|
||||||
|
server_url="https://tu-couchdb",
|
||||||
|
dbname="telesec",
|
||||||
|
username="usuario",
|
||||||
|
password="clave",
|
||||||
|
secret="SECRET123",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Guardar cifrado (como TS_encrypt)
|
||||||
|
db.put("personas", "abc123", {"nombre": "Ana"}, encrypt=True)
|
||||||
|
|
||||||
|
# Leer y descifrar
|
||||||
|
obj = db.get("personas", "abc123", decrypt=True)
|
||||||
|
|
||||||
|
# Listar una tabla
|
||||||
|
rows = db.list("personas", decrypt=True)
|
||||||
|
for row in rows:
|
||||||
|
print(row.id, row.data)
|
||||||
|
```
|
||||||
|
|
||||||
|
API principal:
|
||||||
|
|
||||||
|
- `TeleSecCouchDB.put(table, item_id, data, encrypt=True)`
|
||||||
|
- `TeleSecCouchDB.get(table, item_id, decrypt=True)`
|
||||||
|
- `TeleSecCouchDB.list(table, decrypt=True)`
|
||||||
|
- `TeleSecCouchDB.delete(table, item_id)`
|
||||||
|
- `ts_encrypt(value, secret)` / `ts_decrypt(value, secret)`
|
||||||
|
|
||||||
|
## Agente Windows (Gest-Aula > Ordenadores)
|
||||||
|
|
||||||
|
Se añadió soporte para control de ordenadores del aula:
|
||||||
|
|
||||||
|
- Tabla: `aulas_ordenadores`
|
||||||
|
- Campos reportados por agente: `Hostname`, `UsuarioActual`, `AppActualEjecutable`, `AppActualTitulo`, `LastSeenAt`
|
||||||
|
- Control remoto: `ShutdownBeforeDate` (programado desde web a `hora_servidor + 2 minutos`)
|
||||||
|
|
||||||
|
### Ejecutar agente en Windows
|
||||||
|
|
||||||
|
El agente usa un archivo de configuración en la carpeta personal del usuario:
|
||||||
|
|
||||||
|
- Ruta por defecto: `~/.telesec/windows_agent.json`
|
||||||
|
- Se crea automáticamente si no existe
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m python_sdk.windows_agent --once
|
||||||
|
```
|
||||||
|
|
||||||
|
Ejemplo del JSON de configuración:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server": "https://tu-couchdb",
|
||||||
|
"db": "telesec",
|
||||||
|
"user": "usuario",
|
||||||
|
"password": "clave",
|
||||||
|
"secret": "SECRET123",
|
||||||
|
"machine_id": "",
|
||||||
|
"interval": 15
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
También puedes sobrescribir valores por CLI (`--server`, `--secret`, etc.).
|
||||||
|
|
||||||
|
Opciones útiles:
|
||||||
|
|
||||||
|
- `--once`: una sola iteración
|
||||||
|
- `--interval 15`: intervalo (segundos)
|
||||||
|
- `--dry-run`: no apaga realmente, solo simula
|
||||||
|
- `--config <ruta>`: ruta alternativa del archivo JSON
|
||||||
|
|
||||||
|
### Hora de servidor (sin depender del reloj local)
|
||||||
|
|
||||||
|
El frontend y el agente usan la hora del servidor (cabecera HTTP `Date` de CouchDB) para comparar `ShutdownBeforeDate`.
|
||||||
|
|||||||
35
assets/static/aes.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
CryptoJS v3.1.2
|
||||||
|
code.google.com/p/crypto-js
|
||||||
|
(c) 2009-2013 by Jeff Mott. All rights reserved.
|
||||||
|
code.google.com/p/crypto-js/wiki/License
|
||||||
|
*/
|
||||||
|
var CryptoJS=CryptoJS||function(u,p){var d={},l=d.lib={},s=function(){},t=l.Base={extend:function(a){s.prototype=this;var c=new s;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
|
||||||
|
r=l.WordArray=t.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=p?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,e=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var k=0;k<a;k++)c[j+k>>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535<e.length)for(k=0;k<a;k+=4)c[j+k>>>2]=e[k>>>2];else c.push.apply(c,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
|
||||||
|
32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],e=0;e<a;e+=4)c.push(4294967296*u.random()|0);return new r.init(c,a)}}),w=d.enc={},v=w.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j<a;j++){var k=c[j>>>2]>>>24-8*(j%4)&255;e.push((k>>>4).toString(16));e.push((k&15).toString(16))}return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j<c;j+=2)e[j>>>3]|=parseInt(a.substr(j,
|
||||||
|
2),16)<<24-4*(j%8);return new r.init(e,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j<a;j++)e.push(String.fromCharCode(c[j>>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j<c;j++)e[j>>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new r.init(e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}},
|
||||||
|
q=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,e=c.words,j=c.sigBytes,k=this.blockSize,b=j/(4*k),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*k;j=u.min(4*a,j);if(a){for(var q=0;q<a;q+=k)this._doProcessBlock(e,q);q=e.splice(0,a);c.sigBytes-=j}return new r.init(q,j)},clone:function(){var a=t.clone.call(this);
|
||||||
|
a._data=this._data.clone();return a},_minBufferSize:0});l.Hasher=q.extend({cfg:t.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){q.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,e){return(new a.init(e)).finalize(b)}},_createHmacHelper:function(a){return function(b,e){return(new n.HMAC.init(a,
|
||||||
|
e)).finalize(b)}}});var n=d.algo={};return d}(Math);
|
||||||
|
(function(){var u=CryptoJS,p=u.lib.WordArray;u.enc.Base64={stringify:function(d){var l=d.words,p=d.sigBytes,t=this._map;d.clamp();d=[];for(var r=0;r<p;r+=3)for(var w=(l[r>>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v<p;v++)d.push(t.charAt(w>>>6*(3-v)&63));if(l=t.charAt(64))for(;d.length%4;)d.push(l);return d.join("")},parse:function(d){var l=d.length,s=this._map,t=s.charAt(64);t&&(t=d.indexOf(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w<
|
||||||
|
l;w++)if(w%4){var v=s.indexOf(d.charAt(w-1))<<2*(w%4),b=s.indexOf(d.charAt(w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return p.create(t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})();
|
||||||
|
(function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<<j|b>>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<<j|b>>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<<j|b>>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<<j|b>>>32-j)+n}for(var t=CryptoJS,r=t.lib,w=r.WordArray,v=r.Hasher,r=t.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;r=r.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])},
|
||||||
|
_doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]),
|
||||||
|
f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f,
|
||||||
|
m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m,
|
||||||
|
E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;n[c>>>5]|=128<<24-c%32;var e=u.floor(a/
|
||||||
|
4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(n.length+1);this._process();b=this._hash;n=b.words;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math);
|
||||||
|
(function(){var u=CryptoJS,p=u.lib,d=p.Base,l=p.WordArray,p=u.algo,s=p.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,r){for(var p=this.cfg,s=p.hasher.create(),b=l.create(),u=b.words,q=p.keySize,p=p.iterations;u.length<q;){n&&s.update(n);var n=s.update(d).finalize(r);s.reset();for(var a=1;a<p;a++)n=s.finalize(n),s.reset();b.concat(n)}b.sigBytes=4*q;return b}});u.EvpKDF=function(d,l,p){return s.create(p).compute(d,
|
||||||
|
l)}})();
|
||||||
|
CryptoJS.lib.Cipher||function(u){var p=CryptoJS,d=p.lib,l=d.Base,s=d.WordArray,t=d.BufferedBlockAlgorithm,r=p.enc.Base64,w=p.algo.EvpKDF,v=d.Cipher=t.extend({cfg:l.extend(),createEncryptor:function(e,a){return this.create(this._ENC_XFORM_MODE,e,a)},createDecryptor:function(e,a){return this.create(this._DEC_XFORM_MODE,e,a)},init:function(e,a,b){this.cfg=this.cfg.extend(b);this._xformMode=e;this._key=a;this.reset()},reset:function(){t.reset.call(this);this._doReset()},process:function(e){this._append(e);return this._process()},
|
||||||
|
finalize:function(e){e&&this._append(e);return this._doFinalize()},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(e){return{encrypt:function(b,k,d){return("string"==typeof k?c:a).encrypt(e,b,k,d)},decrypt:function(b,k,d){return("string"==typeof k?c:a).decrypt(e,b,k,d)}}}});d.StreamCipher=v.extend({_doFinalize:function(){return this._process(!0)},blockSize:1});var b=p.mode={},x=function(e,a,b){var c=this._iv;c?this._iv=u:c=this._prevBlock;for(var d=0;d<b;d++)e[a+d]^=
|
||||||
|
c[d]},q=(d.BlockCipherMode=l.extend({createEncryptor:function(e,a){return this.Encryptor.create(e,a)},createDecryptor:function(e,a){return this.Decryptor.create(e,a)},init:function(e,a){this._cipher=e;this._iv=a}})).extend();q.Encryptor=q.extend({processBlock:function(e,a){var b=this._cipher,c=b.blockSize;x.call(this,e,a,c);b.encryptBlock(e,a);this._prevBlock=e.slice(a,a+c)}});q.Decryptor=q.extend({processBlock:function(e,a){var b=this._cipher,c=b.blockSize,d=e.slice(a,a+c);b.decryptBlock(e,a);x.call(this,
|
||||||
|
e,a,c);this._prevBlock=d}});b=b.CBC=q;q=(p.pad={}).Pkcs7={pad:function(a,b){for(var c=4*b,c=c-a.sigBytes%c,d=c<<24|c<<16|c<<8|c,l=[],n=0;n<c;n+=4)l.push(d);c=s.create(l,c);a.concat(c)},unpad:function(a){a.sigBytes-=a.words[a.sigBytes-1>>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:q}),reset:function(){v.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a,
|
||||||
|
this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var n=d.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(p.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?s.create([1398893684,
|
||||||
|
1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=r.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=s.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return n.create({ciphertext:a,salt:c})}},a=d.SerializableCipher=l.extend({cfg:l.extend({format:b}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var l=a.createEncryptor(c,d);b=l.finalize(b);l=l.cfg;return n.create({ciphertext:b,key:c,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})},
|
||||||
|
decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),p=(p.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=s.random(8));a=w.create({keySize:b+c}).compute(a,d);c=s.create(a.words.slice(b),4*c);a.sigBytes=4*b;return n.create({key:a,iv:c,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:p}),encrypt:function(b,c,d,l){l=this.cfg.extend(l);d=l.kdf.execute(d,
|
||||||
|
b.keySize,b.ivSize);l.iv=d.iv;b=a.encrypt.call(this,b,c,d.key,l);b.mixIn(d);return b},decrypt:function(b,c,d,l){l=this.cfg.extend(l);c=this._parse(c,l.format);d=l.kdf.execute(d,b.keySize,b.ivSize,c.salt);l.iv=d.iv;return a.decrypt.call(this,b,c,d.key,l)}})}();
|
||||||
|
(function(){for(var u=CryptoJS,p=u.lib.BlockCipher,d=u.algo,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8,
|
||||||
|
16,32,64,128,27,54],d=d.AES=p.extend({_doReset:function(){for(var a=this._key,c=a.words,d=a.sigBytes/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j<a;j++)if(j<d)e[j]=c[j];else{var k=e[j-1];j%d?6<d&&4==j%d&&(k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;d<a;d++)j=a-d,k=d%4?e[j]:e[j-4],c[d]=4>d||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>>
|
||||||
|
8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r<m;r++)var q=d[g>>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t=
|
||||||
|
d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});u.AES=p._createHelper(d)})();
|
||||||
BIN
assets/static/appico/message.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
assets/static/appico/piggy_bank.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
@@ -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){}
|
|
||||||
}());
|
|
||||||
BIN
assets/static/cash_flow.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
20
assets/static/chart.umd.min.js
vendored
Normal file
@@ -429,3 +429,46 @@ fieldset legend {
|
|||||||
pre {
|
pre {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
.picto {
|
||||||
|
min-height: 125px;
|
||||||
|
width: 100px;
|
||||||
|
border: 2.5px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
background: white;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.picto b {
|
||||||
|
padding-top: 40px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-option input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.panel-option:has(input:checked) {
|
||||||
|
background-color: #ccc;
|
||||||
|
outline: 5px solid blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveico {
|
||||||
|
border-color: green !important;
|
||||||
|
}
|
||||||
|
.delico {
|
||||||
|
border-color: red !important;
|
||||||
|
}
|
||||||
|
.opicon {
|
||||||
|
border-color: blue !important;
|
||||||
|
}
|
||||||
|
.saveico img, .delico img, .opicon img {
|
||||||
|
height: 52px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.saveico, .delico, .opicon {
|
||||||
|
padding: 2.5px 7.5px;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 4px solid;
|
||||||
|
}
|
||||||
BIN
assets/static/exchange.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
assets/static/exit.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/static/find.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
assets/static/floppy_disk_green.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/static/garbage.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
2313
assets/static/gun.js
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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".
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
7
assets/static/pouchdb.min.js
vendored
Normal file
BIN
assets/static/printer2.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
@@ -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;
|
|
||||||
|
|
||||||
}());
|
|
||||||
@@ -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);
|
|
||||||
|
|
||||||
}());
|
|
||||||
@@ -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){}
|
|
||||||
|
|
||||||
}());
|
|
||||||
1537
assets/static/sea.js
7
assets/static/simplemde.min.css
vendored
15
assets/static/simplemde.min.js
vendored
@@ -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!
|
|
||||||
});
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
|
|
||||||
}());
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}());
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}());
|
|
||||||
5
build.py
@@ -2,6 +2,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
def get_all_files(directory):
|
def get_all_files(directory):
|
||||||
files = []
|
files = []
|
||||||
@@ -14,7 +15,7 @@ def get_all_files(directory):
|
|||||||
return files
|
return files
|
||||||
|
|
||||||
PREFETCH = ""
|
PREFETCH = ""
|
||||||
VERSIONCO = "2025-08"
|
VERSIONCO = "2026-02-23_" + time.strftime("%Y%m%d%H%M%S")
|
||||||
HANDLEPARSE = get_all_files("src")
|
HANDLEPARSE = get_all_files("src")
|
||||||
TITLE = os.environ.get("TELESEC_TITLE", "TeleSec")
|
TITLE = os.environ.get("TELESEC_TITLE", "TeleSec")
|
||||||
HOSTER = os.environ.get("TELESEC_HOSTER", "EuskadiTech")
|
HOSTER = os.environ.get("TELESEC_HOSTER", "EuskadiTech")
|
||||||
@@ -35,7 +36,7 @@ shutil.copytree("assets","dist", dirs_exist_ok=True)
|
|||||||
def replace_handles(string):
|
def replace_handles(string):
|
||||||
string = string.replace("%%PREFETCH%%", PREFETCH)
|
string = string.replace("%%PREFETCH%%", PREFETCH)
|
||||||
string = string.replace("%%VERSIONCO%%", VERSIONCO)
|
string = string.replace("%%VERSIONCO%%", VERSIONCO)
|
||||||
string = string.replace("%%TITLE%%", "TeleSec")
|
string = string.replace("%%TITLE%%", TITLE)
|
||||||
string = string.replace("%%HOSTER%%", HOSTER)
|
string = string.replace("%%HOSTER%%", HOSTER)
|
||||||
string = string.replace("%%ASSETSJSON%%", json.dumps(ASSETS, ensure_ascii=False))
|
string = string.replace("%%ASSETSJSON%%", json.dumps(ASSETS, ensure_ascii=False))
|
||||||
return string
|
return string
|
||||||
|
|||||||
293
python_sdk/telesec_couchdb.py
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
import base64
|
||||||
|
import email.utils
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
|
||||||
|
|
||||||
|
class TeleSecCryptoError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TeleSecCouchDBError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
|
||||||
|
pad_len = block_size - (len(data) % block_size)
|
||||||
|
return data + bytes([pad_len]) * pad_len
|
||||||
|
|
||||||
|
|
||||||
|
def _pkcs7_unpad(data: bytes, block_size: int = 16) -> bytes:
|
||||||
|
if not data or len(data) % block_size != 0:
|
||||||
|
raise TeleSecCryptoError("Invalid padded data length")
|
||||||
|
pad_len = data[-1]
|
||||||
|
if pad_len < 1 or pad_len > block_size:
|
||||||
|
raise TeleSecCryptoError("Invalid PKCS7 padding")
|
||||||
|
if data[-pad_len:] != bytes([pad_len]) * pad_len:
|
||||||
|
raise TeleSecCryptoError("Invalid PKCS7 padding bytes")
|
||||||
|
return data[:-pad_len]
|
||||||
|
|
||||||
|
|
||||||
|
def _evp_bytes_to_key(passphrase: bytes, salt: bytes, key_len: int, iv_len: int) -> tuple[bytes, bytes]:
|
||||||
|
d = b""
|
||||||
|
prev = b""
|
||||||
|
while len(d) < key_len + iv_len:
|
||||||
|
prev = hashlib.md5(prev + passphrase + salt).digest()
|
||||||
|
d += prev
|
||||||
|
return d[:key_len], d[key_len : key_len + iv_len]
|
||||||
|
|
||||||
|
|
||||||
|
def _json_dumps_like_js(value: Any) -> str:
|
||||||
|
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
|
||||||
|
|
||||||
|
def ts_encrypt(input_value: Any, secret: str) -> str:
|
||||||
|
"""
|
||||||
|
Compatible with JS: CryptoJS.AES.encrypt(payload, secret).toString()
|
||||||
|
wrapped as RSA{<ciphertext>}.
|
||||||
|
"""
|
||||||
|
if secret is None or secret == "":
|
||||||
|
if isinstance(input_value, str):
|
||||||
|
return input_value
|
||||||
|
return _json_dumps_like_js(input_value)
|
||||||
|
|
||||||
|
payload = input_value
|
||||||
|
if not isinstance(input_value, str):
|
||||||
|
try:
|
||||||
|
payload = _json_dumps_like_js(input_value)
|
||||||
|
except Exception:
|
||||||
|
payload = str(input_value)
|
||||||
|
|
||||||
|
payload_bytes = payload.encode("utf-8")
|
||||||
|
salt = os.urandom(8)
|
||||||
|
key, iv = _evp_bytes_to_key(secret.encode("utf-8"), salt, 32, 16)
|
||||||
|
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
|
||||||
|
encrypted = cipher.encrypt(_pkcs7_pad(payload_bytes, 16))
|
||||||
|
openssl_blob = b"Salted__" + salt + encrypted
|
||||||
|
b64 = base64.b64encode(openssl_blob).decode("utf-8")
|
||||||
|
return f"RSA{{{b64}}}"
|
||||||
|
|
||||||
|
|
||||||
|
def ts_decrypt(input_value: Any, secret: str) -> Any:
|
||||||
|
"""
|
||||||
|
Compatible with JS TS_decrypt behavior:
|
||||||
|
- If not string: return as-is.
|
||||||
|
- If RSA{...}: decrypt AES(CryptoJS passphrase mode), parse JSON when possible.
|
||||||
|
- If plain string JSON: parse JSON.
|
||||||
|
- Else: return raw string.
|
||||||
|
"""
|
||||||
|
if not isinstance(input_value, str):
|
||||||
|
return input_value
|
||||||
|
|
||||||
|
is_wrapped = input_value.startswith("RSA{") and input_value.endswith("}")
|
||||||
|
if is_wrapped:
|
||||||
|
if not secret:
|
||||||
|
raise TeleSecCryptoError("Secret is required to decrypt RSA payload")
|
||||||
|
b64 = input_value[4:-1]
|
||||||
|
try:
|
||||||
|
raw = base64.b64decode(b64)
|
||||||
|
except Exception as exc:
|
||||||
|
raise TeleSecCryptoError("Invalid base64 payload") from exc
|
||||||
|
|
||||||
|
if len(raw) < 16 or not raw.startswith(b"Salted__"):
|
||||||
|
raise TeleSecCryptoError("Unsupported encrypted payload format")
|
||||||
|
|
||||||
|
salt = raw[8:16]
|
||||||
|
ciphertext = raw[16:]
|
||||||
|
key, iv = _evp_bytes_to_key(secret.encode("utf-8"), salt, 32, 16)
|
||||||
|
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
|
||||||
|
decrypted = cipher.decrypt(ciphertext)
|
||||||
|
decrypted = _pkcs7_unpad(decrypted, 16)
|
||||||
|
|
||||||
|
try:
|
||||||
|
text = decrypted.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
text = decrypted.decode("latin-1")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(text)
|
||||||
|
except Exception:
|
||||||
|
return text
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(input_value)
|
||||||
|
except Exception:
|
||||||
|
return input_value
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TeleSecDoc:
|
||||||
|
id: str
|
||||||
|
data: Any
|
||||||
|
raw: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class TeleSecCouchDB:
|
||||||
|
"""
|
||||||
|
Direct CouchDB client for TeleSec docs (_id = "<table>:<id>").
|
||||||
|
No local replication layer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
server_url: str,
|
||||||
|
dbname: str,
|
||||||
|
secret: Optional[str] = None,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
password: Optional[str] = None,
|
||||||
|
timeout: int = 30,
|
||||||
|
session: Optional[requests.Session] = None,
|
||||||
|
) -> None:
|
||||||
|
self.server_url = server_url.rstrip("/")
|
||||||
|
self.dbname = dbname
|
||||||
|
self.secret = secret or ""
|
||||||
|
self.timeout = timeout
|
||||||
|
self.base_url = f"{self.server_url}/{quote(self.dbname, safe='')}"
|
||||||
|
self.session = session or requests.Session()
|
||||||
|
self.session.headers.update({"Accept": "application/json"})
|
||||||
|
if username is not None:
|
||||||
|
self.session.auth = (username, password or "")
|
||||||
|
|
||||||
|
def _iso_now(self) -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||||
|
|
||||||
|
def _doc_id(self, table: str, item_id: str) -> str:
|
||||||
|
return f"{table}:{item_id}"
|
||||||
|
|
||||||
|
def _request(self, method: str, path: str = "", **kwargs) -> requests.Response:
|
||||||
|
url = self.base_url if not path else f"{self.base_url}/{path.lstrip('/')}"
|
||||||
|
kwargs.setdefault("timeout", self.timeout)
|
||||||
|
res = self.session.request(method=method, url=url, **kwargs)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_server_datetime(self) -> datetime:
|
||||||
|
"""
|
||||||
|
Returns server datetime using HTTP Date header from CouchDB.
|
||||||
|
Avoids reliance on local machine clock.
|
||||||
|
"""
|
||||||
|
candidates = [
|
||||||
|
("HEAD", self.base_url),
|
||||||
|
("GET", self.base_url),
|
||||||
|
("HEAD", self.server_url),
|
||||||
|
("GET", self.server_url),
|
||||||
|
]
|
||||||
|
for method, url in candidates:
|
||||||
|
try:
|
||||||
|
res = self.session.request(method=method, url=url, timeout=self.timeout)
|
||||||
|
date_header = res.headers.get("Date")
|
||||||
|
if not date_header:
|
||||||
|
continue
|
||||||
|
dt = email.utils.parsedate_to_datetime(date_header)
|
||||||
|
if dt is None:
|
||||||
|
continue
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
|
return dt.astimezone(timezone.utc)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
raise TeleSecCouchDBError("Unable to retrieve server time from CouchDB Date header")
|
||||||
|
|
||||||
|
def iso_from_server_plus_minutes(self, minutes: int = 0) -> str:
|
||||||
|
now = self.get_server_datetime()
|
||||||
|
target = now.timestamp() + (minutes * 60)
|
||||||
|
out = datetime.fromtimestamp(target, tz=timezone.utc)
|
||||||
|
return out.isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||||
|
|
||||||
|
def check_connection(self) -> Dict[str, Any]:
|
||||||
|
res = self._request("GET")
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise TeleSecCouchDBError(f"CouchDB connection failed: {res.status_code} {res.text}")
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
def get_raw(self, doc_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
res = self._request("GET", quote(doc_id, safe=""))
|
||||||
|
if res.status_code == 404:
|
||||||
|
return None
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise TeleSecCouchDBError(f"GET doc failed: {res.status_code} {res.text}")
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
def put_raw(self, doc: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
if "_id" not in doc:
|
||||||
|
raise ValueError("Document must include _id")
|
||||||
|
res = self._request(
|
||||||
|
"PUT",
|
||||||
|
quote(doc["_id"], safe=""),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
data=_json_dumps_like_js(doc).encode("utf-8"),
|
||||||
|
)
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise TeleSecCouchDBError(f"PUT doc failed: {res.status_code} {res.text}")
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
def delete_raw(self, doc_id: str) -> bool:
|
||||||
|
doc = self.get_raw(doc_id)
|
||||||
|
if not doc:
|
||||||
|
return False
|
||||||
|
res = self._request("DELETE", f"{quote(doc_id, safe='')}?rev={quote(doc['_rev'], safe='')}")
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise TeleSecCouchDBError(f"DELETE doc failed: {res.status_code} {res.text}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def put(self, table: str, item_id: str, data: Any, encrypt: bool = True) -> Dict[str, Any]:
|
||||||
|
doc_id = self._doc_id(table, item_id)
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
self.delete_raw(doc_id)
|
||||||
|
return {"ok": True, "id": doc_id, "deleted": True}
|
||||||
|
|
||||||
|
existing = self.get_raw(doc_id)
|
||||||
|
doc: Dict[str, Any] = existing if existing else {"_id": doc_id}
|
||||||
|
|
||||||
|
to_store = data
|
||||||
|
is_encrypted_string = isinstance(data, str) and data.startswith("RSA{") and data.endswith("}")
|
||||||
|
if encrypt and self.secret and not is_encrypted_string:
|
||||||
|
to_store = ts_encrypt(data, self.secret)
|
||||||
|
|
||||||
|
doc["data"] = to_store
|
||||||
|
doc["table"] = table
|
||||||
|
doc["ts"] = self._iso_now()
|
||||||
|
|
||||||
|
return self.put_raw(doc)
|
||||||
|
|
||||||
|
def get(self, table: str, item_id: str, decrypt: bool = True) -> Optional[Any]:
|
||||||
|
doc_id = self._doc_id(table, item_id)
|
||||||
|
doc = self.get_raw(doc_id)
|
||||||
|
if not doc:
|
||||||
|
return None
|
||||||
|
value = doc.get("data")
|
||||||
|
if decrypt:
|
||||||
|
return ts_decrypt(value, self.secret)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def delete(self, table: str, item_id: str) -> bool:
|
||||||
|
return self.delete_raw(self._doc_id(table, item_id))
|
||||||
|
|
||||||
|
def list(self, table: str, decrypt: bool = True) -> List[TeleSecDoc]:
|
||||||
|
params = {
|
||||||
|
"include_docs": "true",
|
||||||
|
"startkey": f'"{table}:"',
|
||||||
|
"endkey": f'"{table}:\uffff"',
|
||||||
|
}
|
||||||
|
res = self._request("GET", "_all_docs", params=params)
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise TeleSecCouchDBError(f"LIST docs failed: {res.status_code} {res.text}")
|
||||||
|
|
||||||
|
rows = res.json().get("rows", [])
|
||||||
|
out: List[TeleSecDoc] = []
|
||||||
|
for row in rows:
|
||||||
|
doc = row.get("doc") or {}
|
||||||
|
item_id = row.get("id", "").split(":", 1)[1] if ":" in row.get("id", "") else row.get("id", "")
|
||||||
|
value = doc.get("data")
|
||||||
|
if decrypt:
|
||||||
|
value = ts_decrypt(value, self.secret)
|
||||||
|
out.append(TeleSecDoc(id=item_id, data=value, raw=doc))
|
||||||
|
return out
|
||||||
545
python_sdk/windows_agent.py
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
import argparse
|
||||||
|
import ctypes
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any, Dict, Optional, List
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
import base64
|
||||||
|
import email.utils
|
||||||
|
import hashlib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
|
||||||
|
|
||||||
|
class TeleSecCryptoError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TeleSecCouchDBError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
|
||||||
|
pad_len = block_size - (len(data) % block_size)
|
||||||
|
return data + bytes([pad_len]) * pad_len
|
||||||
|
|
||||||
|
|
||||||
|
def _pkcs7_unpad(data: bytes, block_size: int = 16) -> bytes:
|
||||||
|
if not data or len(data) % block_size != 0:
|
||||||
|
raise TeleSecCryptoError("Invalid padded data length")
|
||||||
|
pad_len = data[-1]
|
||||||
|
if pad_len < 1 or pad_len > block_size:
|
||||||
|
raise TeleSecCryptoError("Invalid PKCS7 padding")
|
||||||
|
if data[-pad_len:] != bytes([pad_len]) * pad_len:
|
||||||
|
raise TeleSecCryptoError("Invalid PKCS7 padding bytes")
|
||||||
|
return data[:-pad_len]
|
||||||
|
|
||||||
|
|
||||||
|
def _evp_bytes_to_key(passphrase: bytes, salt: bytes, key_len: int, iv_len: int) -> tuple[bytes, bytes]:
|
||||||
|
d = b""
|
||||||
|
prev = b""
|
||||||
|
while len(d) < key_len + iv_len:
|
||||||
|
prev = hashlib.md5(prev + passphrase + salt).digest()
|
||||||
|
d += prev
|
||||||
|
return d[:key_len], d[key_len : key_len + iv_len]
|
||||||
|
|
||||||
|
|
||||||
|
def _json_dumps_like_js(value: Any) -> str:
|
||||||
|
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
|
||||||
|
|
||||||
|
def ts_encrypt(input_value: Any, secret: str) -> str:
|
||||||
|
"""
|
||||||
|
Compatible with JS: CryptoJS.AES.encrypt(payload, secret).toString()
|
||||||
|
wrapped as RSA{<ciphertext>}.
|
||||||
|
"""
|
||||||
|
if secret is None or secret == "":
|
||||||
|
if isinstance(input_value, str):
|
||||||
|
return input_value
|
||||||
|
return _json_dumps_like_js(input_value)
|
||||||
|
|
||||||
|
payload = input_value
|
||||||
|
if not isinstance(input_value, str):
|
||||||
|
try:
|
||||||
|
payload = _json_dumps_like_js(input_value)
|
||||||
|
except Exception:
|
||||||
|
payload = str(input_value)
|
||||||
|
|
||||||
|
payload_bytes = payload.encode("utf-8")
|
||||||
|
salt = os.urandom(8)
|
||||||
|
key, iv = _evp_bytes_to_key(secret.encode("utf-8"), salt, 32, 16)
|
||||||
|
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
|
||||||
|
encrypted = cipher.encrypt(_pkcs7_pad(payload_bytes, 16))
|
||||||
|
openssl_blob = b"Salted__" + salt + encrypted
|
||||||
|
b64 = base64.b64encode(openssl_blob).decode("utf-8")
|
||||||
|
return f"RSA{{{b64}}}"
|
||||||
|
|
||||||
|
|
||||||
|
def ts_decrypt(input_value: Any, secret: str) -> Any:
|
||||||
|
"""
|
||||||
|
Compatible with JS TS_decrypt behavior:
|
||||||
|
- If not string: return as-is.
|
||||||
|
- If RSA{...}: decrypt AES(CryptoJS passphrase mode), parse JSON when possible.
|
||||||
|
- If plain string JSON: parse JSON.
|
||||||
|
- Else: return raw string.
|
||||||
|
"""
|
||||||
|
if not isinstance(input_value, str):
|
||||||
|
return input_value
|
||||||
|
|
||||||
|
is_wrapped = input_value.startswith("RSA{") and input_value.endswith("}")
|
||||||
|
if is_wrapped:
|
||||||
|
if not secret:
|
||||||
|
raise TeleSecCryptoError("Secret is required to decrypt RSA payload")
|
||||||
|
b64 = input_value[4:-1]
|
||||||
|
try:
|
||||||
|
raw = base64.b64decode(b64)
|
||||||
|
except Exception as exc:
|
||||||
|
raise TeleSecCryptoError("Invalid base64 payload") from exc
|
||||||
|
|
||||||
|
if len(raw) < 16 or not raw.startswith(b"Salted__"):
|
||||||
|
raise TeleSecCryptoError("Unsupported encrypted payload format")
|
||||||
|
|
||||||
|
salt = raw[8:16]
|
||||||
|
ciphertext = raw[16:]
|
||||||
|
key, iv = _evp_bytes_to_key(secret.encode("utf-8"), salt, 32, 16)
|
||||||
|
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
|
||||||
|
decrypted = cipher.decrypt(ciphertext)
|
||||||
|
decrypted = _pkcs7_unpad(decrypted, 16)
|
||||||
|
|
||||||
|
try:
|
||||||
|
text = decrypted.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
text = decrypted.decode("latin-1")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(text)
|
||||||
|
except Exception:
|
||||||
|
return text
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(input_value)
|
||||||
|
except Exception:
|
||||||
|
return input_value
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TeleSecDoc:
|
||||||
|
id: str
|
||||||
|
data: Any
|
||||||
|
raw: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class TeleSecCouchDB:
|
||||||
|
"""
|
||||||
|
Direct CouchDB client for TeleSec docs (_id = "<table>:<id>").
|
||||||
|
No local replication layer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
server_url: str,
|
||||||
|
dbname: str,
|
||||||
|
secret: Optional[str] = None,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
password: Optional[str] = None,
|
||||||
|
timeout: int = 30,
|
||||||
|
session: Optional[requests.Session] = None,
|
||||||
|
) -> None:
|
||||||
|
self.server_url = server_url.rstrip("/")
|
||||||
|
self.dbname = dbname
|
||||||
|
self.secret = secret or ""
|
||||||
|
self.timeout = timeout
|
||||||
|
self.base_url = f"{self.server_url}/{quote(self.dbname, safe='')}"
|
||||||
|
self.session = session or requests.Session()
|
||||||
|
self.session.headers.update({"Accept": "application/json"})
|
||||||
|
if username is not None:
|
||||||
|
self.session.auth = (username, password or "")
|
||||||
|
|
||||||
|
def _iso_now(self) -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||||
|
|
||||||
|
def _doc_id(self, table: str, item_id: str) -> str:
|
||||||
|
return f"{table}:{item_id}"
|
||||||
|
|
||||||
|
def _request(self, method: str, path: str = "", **kwargs) -> requests.Response:
|
||||||
|
url = self.base_url if not path else f"{self.base_url}/{path.lstrip('/')}"
|
||||||
|
kwargs.setdefault("timeout", self.timeout)
|
||||||
|
res = self.session.request(method=method, url=url, **kwargs)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_server_datetime(self) -> datetime:
|
||||||
|
"""
|
||||||
|
Returns server datetime using HTTP Date header from CouchDB.
|
||||||
|
Avoids reliance on local machine clock.
|
||||||
|
"""
|
||||||
|
candidates = [
|
||||||
|
("HEAD", self.base_url),
|
||||||
|
("GET", self.base_url),
|
||||||
|
("HEAD", self.server_url),
|
||||||
|
("GET", self.server_url),
|
||||||
|
]
|
||||||
|
for method, url in candidates:
|
||||||
|
try:
|
||||||
|
res = self.session.request(method=method, url=url, timeout=self.timeout)
|
||||||
|
date_header = res.headers.get("Date")
|
||||||
|
if not date_header:
|
||||||
|
continue
|
||||||
|
dt = email.utils.parsedate_to_datetime(date_header)
|
||||||
|
if dt is None:
|
||||||
|
continue
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
|
return dt.astimezone(timezone.utc)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
raise TeleSecCouchDBError("Unable to retrieve server time from CouchDB Date header")
|
||||||
|
|
||||||
|
def iso_from_server_plus_minutes(self, minutes: int = 0) -> str:
|
||||||
|
now = self.get_server_datetime()
|
||||||
|
target = now.timestamp() + (minutes * 60)
|
||||||
|
out = datetime.fromtimestamp(target, tz=timezone.utc)
|
||||||
|
return out.isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||||
|
|
||||||
|
def check_connection(self) -> Dict[str, Any]:
|
||||||
|
res = self._request("GET")
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise TeleSecCouchDBError(f"CouchDB connection failed: {res.status_code} {res.text}")
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
def get_raw(self, doc_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
res = self._request("GET", quote(doc_id, safe=""))
|
||||||
|
if res.status_code == 404:
|
||||||
|
return None
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise TeleSecCouchDBError(f"GET doc failed: {res.status_code} {res.text}")
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
def put_raw(self, doc: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
if "_id" not in doc:
|
||||||
|
raise ValueError("Document must include _id")
|
||||||
|
res = self._request(
|
||||||
|
"PUT",
|
||||||
|
quote(doc["_id"], safe=""),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
data=_json_dumps_like_js(doc).encode("utf-8"),
|
||||||
|
)
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise TeleSecCouchDBError(f"PUT doc failed: {res.status_code} {res.text}")
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
def delete_raw(self, doc_id: str) -> bool:
|
||||||
|
doc = self.get_raw(doc_id)
|
||||||
|
if not doc:
|
||||||
|
return False
|
||||||
|
res = self._request("DELETE", f"{quote(doc_id, safe='')}?rev={quote(doc['_rev'], safe='')}")
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise TeleSecCouchDBError(f"DELETE doc failed: {res.status_code} {res.text}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def put(self, table: str, item_id: str, data: Any, encrypt: bool = True) -> Dict[str, Any]:
|
||||||
|
doc_id = self._doc_id(table, item_id)
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
self.delete_raw(doc_id)
|
||||||
|
return {"ok": True, "id": doc_id, "deleted": True}
|
||||||
|
|
||||||
|
existing = self.get_raw(doc_id)
|
||||||
|
doc: Dict[str, Any] = existing if existing else {"_id": doc_id}
|
||||||
|
|
||||||
|
to_store = data
|
||||||
|
is_encrypted_string = isinstance(data, str) and data.startswith("RSA{") and data.endswith("}")
|
||||||
|
if encrypt and self.secret and not is_encrypted_string:
|
||||||
|
to_store = ts_encrypt(data, self.secret)
|
||||||
|
|
||||||
|
doc["data"] = to_store
|
||||||
|
doc["table"] = table
|
||||||
|
doc["ts"] = self._iso_now()
|
||||||
|
|
||||||
|
return self.put_raw(doc)
|
||||||
|
|
||||||
|
def get(self, table: str, item_id: str, decrypt: bool = True) -> Optional[Any]:
|
||||||
|
doc_id = self._doc_id(table, item_id)
|
||||||
|
doc = self.get_raw(doc_id)
|
||||||
|
if not doc:
|
||||||
|
return None
|
||||||
|
value = doc.get("data")
|
||||||
|
if decrypt:
|
||||||
|
return ts_decrypt(value, self.secret)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def delete(self, table: str, item_id: str) -> bool:
|
||||||
|
return self.delete_raw(self._doc_id(table, item_id))
|
||||||
|
|
||||||
|
def list(self, table: str, decrypt: bool = True) -> List[TeleSecDoc]:
|
||||||
|
params = {
|
||||||
|
"include_docs": "true",
|
||||||
|
"startkey": f'"{table}:"',
|
||||||
|
"endkey": f'"{table}:\uffff"',
|
||||||
|
}
|
||||||
|
res = self._request("GET", "_all_docs", params=params)
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise TeleSecCouchDBError(f"LIST docs failed: {res.status_code} {res.text}")
|
||||||
|
|
||||||
|
rows = res.json().get("rows", [])
|
||||||
|
out: List[TeleSecDoc] = []
|
||||||
|
for row in rows:
|
||||||
|
doc = row.get("doc") or {}
|
||||||
|
item_id = row.get("id", "").split(":", 1)[1] if ":" in row.get("id", "") else row.get("id", "")
|
||||||
|
value = doc.get("data")
|
||||||
|
if decrypt:
|
||||||
|
value = ts_decrypt(value, self.secret)
|
||||||
|
out.append(TeleSecDoc(id=item_id, data=value, raw=doc))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def utcnow_iso() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_iso(value: str) -> Optional[datetime]:
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
v = value.strip().replace("Z", "+00:00")
|
||||||
|
dt = datetime.fromisoformat(v)
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
|
return dt.astimezone(timezone.utc)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_username() -> str:
|
||||||
|
try:
|
||||||
|
return psutil.Process().username() or os.getlogin()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
return os.getlogin()
|
||||||
|
except Exception:
|
||||||
|
return os.environ.get("USERNAME", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _window_title(hwnd: int) -> str:
|
||||||
|
buf_len = ctypes.windll.user32.GetWindowTextLengthW(hwnd)
|
||||||
|
if buf_len <= 0:
|
||||||
|
return ""
|
||||||
|
buf = ctypes.create_unicode_buffer(buf_len + 1)
|
||||||
|
ctypes.windll.user32.GetWindowTextW(hwnd, buf, buf_len + 1)
|
||||||
|
return buf.value or ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_app() -> Dict[str, str]:
|
||||||
|
exe = ""
|
||||||
|
title = ""
|
||||||
|
try:
|
||||||
|
hwnd = ctypes.windll.user32.GetForegroundWindow()
|
||||||
|
if hwnd:
|
||||||
|
title = _window_title(hwnd)
|
||||||
|
pid = ctypes.c_ulong()
|
||||||
|
ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
||||||
|
if pid.value:
|
||||||
|
try:
|
||||||
|
proc = psutil.Process(pid.value)
|
||||||
|
exe = proc.name() or ""
|
||||||
|
except Exception:
|
||||||
|
exe = ""
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"exe": exe, "title": title}
|
||||||
|
|
||||||
|
|
||||||
|
def build_payload(machine_id: str) -> Dict[str, Any]:
|
||||||
|
app = get_active_app()
|
||||||
|
return {
|
||||||
|
"Hostname": machine_id,
|
||||||
|
"UsuarioActual": get_current_username(),
|
||||||
|
"AppActualEjecutable": app.get("exe", ""),
|
||||||
|
"AppActualTitulo": app.get("title", ""),
|
||||||
|
# campo local diagnóstico (no se usa para decisión remota)
|
||||||
|
"AgentLocalSeenAt": utcnow_iso(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def should_shutdown(data: Dict[str, Any], server_now: datetime) -> bool:
|
||||||
|
target = parse_iso(str(data.get("ShutdownBeforeDate", "") or ""))
|
||||||
|
if not target:
|
||||||
|
return False
|
||||||
|
return server_now >= target
|
||||||
|
|
||||||
|
|
||||||
|
def execute_shutdown(dry_run: bool = False) -> None:
|
||||||
|
if dry_run:
|
||||||
|
print("[DRY-RUN] Ejecutaría: shutdown /s /t 0 /f")
|
||||||
|
return
|
||||||
|
subprocess.run(["shutdown", "/s", "/t", "0", "/f"], check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def run_once(client: TeleSecCouchDB, machine_id: str, dry_run: bool = False) -> None:
|
||||||
|
server_now = client.get_server_datetime()
|
||||||
|
server_now_iso = server_now.isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||||
|
|
||||||
|
raw = client.get(table="aulas_ordenadores", item_id=machine_id, decrypt=False)
|
||||||
|
current: Dict[str, Any] = {}
|
||||||
|
if raw is not None:
|
||||||
|
current = ts_decrypt(raw, client.secret)
|
||||||
|
if not isinstance(current, dict):
|
||||||
|
current = {}
|
||||||
|
|
||||||
|
update = build_payload(machine_id)
|
||||||
|
update["LastSeenAt"] = server_now_iso
|
||||||
|
|
||||||
|
for key in ["ShutdownBeforeDate", "ShutdownRequestedAt", "ShutdownRequestedBy"]:
|
||||||
|
if key in current:
|
||||||
|
update[key] = current.get(key)
|
||||||
|
|
||||||
|
client.put(table="aulas_ordenadores", item_id=machine_id, data=update, encrypt=True)
|
||||||
|
|
||||||
|
if should_shutdown(update, server_now):
|
||||||
|
print(f"[{server_now_iso}] ShutdownBeforeDate alcanzado. Apagando {machine_id}...")
|
||||||
|
execute_shutdown(dry_run=dry_run)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"[{server_now_iso}] Reportado {machine_id} user={update.get('UsuarioActual','')} "
|
||||||
|
f"exe={update.get('AppActualEjecutable','')} title={update.get('AppActualTitulo','')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="TeleSec Windows Agent")
|
||||||
|
parser.add_argument("--server", default="", help="CouchDB server URL, ej. https://couch.example")
|
||||||
|
parser.add_argument("--db", default="telesec", help="Database name")
|
||||||
|
parser.add_argument("--user", default="", help="CouchDB username")
|
||||||
|
parser.add_argument("--password", default="", help="CouchDB password")
|
||||||
|
parser.add_argument("--secret", default="", help="TeleSec secret para cifrado")
|
||||||
|
parser.add_argument("--machine-id", default="", help="ID de máquina (default: hostname)")
|
||||||
|
parser.add_argument("--interval", type=int, default=15, help="Intervalo en segundos")
|
||||||
|
parser.add_argument("--once", action="store_true", help="Ejecutar una sola iteración")
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="No apagar realmente, solo log")
|
||||||
|
parser.add_argument(
|
||||||
|
"--config",
|
||||||
|
default="",
|
||||||
|
help="Ruta de config JSON (default: ~/.telesec/windows_agent.json)",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def _default_config_path() -> str:
|
||||||
|
return os.path.join(os.path.expanduser("~"), ".telesec", "windows_agent.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_or_init_config(path: str) -> Dict[str, Any]:
|
||||||
|
if not os.path.exists(path):
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
default_cfg = {
|
||||||
|
"server": "https://tu-couchdb",
|
||||||
|
"db": "telesec",
|
||||||
|
"user": "",
|
||||||
|
"password": "",
|
||||||
|
"secret": "",
|
||||||
|
"machine_id": "",
|
||||||
|
"interval": 15,
|
||||||
|
}
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(default_cfg, f, ensure_ascii=False, indent=2)
|
||||||
|
return default_cfg
|
||||||
|
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return data
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _save_config(path: str, data: Dict[str, Any]) -> None:
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def _pick(cli_value: Any, cfg_value: Any, default_value: Any = None) -> Any:
|
||||||
|
if cli_value is None:
|
||||||
|
return cfg_value if cfg_value not in [None, ""] else default_value
|
||||||
|
if isinstance(cli_value, str):
|
||||||
|
if cli_value.strip() == "":
|
||||||
|
return cfg_value if cfg_value not in [None, ""] else default_value
|
||||||
|
return cli_value
|
||||||
|
return cli_value
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
config_path = args.config or _default_config_path()
|
||||||
|
try:
|
||||||
|
cfg = _load_or_init_config(config_path)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"No se pudo cargar/crear config en {config_path}: {exc}", file=sys.stderr)
|
||||||
|
return 3
|
||||||
|
|
||||||
|
server = _pick(args.server, cfg.get("server"), "")
|
||||||
|
db = _pick(args.db, cfg.get("db"), "telesec")
|
||||||
|
user = _pick(args.user, cfg.get("user"), "")
|
||||||
|
password = _pick(args.password, cfg.get("password"), "")
|
||||||
|
secret = _pick(args.secret, cfg.get("secret"), "")
|
||||||
|
machine_id = _pick(args.machine_id, cfg.get("machine_id"), "")
|
||||||
|
interval = _pick(args.interval, cfg.get("interval"), 15)
|
||||||
|
|
||||||
|
machine_id = (machine_id or socket.gethostname() or "unknown-host").strip()
|
||||||
|
|
||||||
|
if not server or not secret:
|
||||||
|
print(
|
||||||
|
"Falta configuración obligatoria. Edita el JSON en: " + config_path,
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 4
|
||||||
|
|
||||||
|
# Persist effective parameters for next runs
|
||||||
|
try:
|
||||||
|
persistent_cfg = {
|
||||||
|
"server": server,
|
||||||
|
"db": db,
|
||||||
|
"user": user,
|
||||||
|
"password": password,
|
||||||
|
"secret": secret,
|
||||||
|
"machine_id": machine_id,
|
||||||
|
"interval": int(interval),
|
||||||
|
}
|
||||||
|
_save_config(config_path, persistent_cfg)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"No se pudo guardar config en {config_path}: {exc}", file=sys.stderr)
|
||||||
|
|
||||||
|
client = TeleSecCouchDB(
|
||||||
|
server_url=server,
|
||||||
|
dbname=db,
|
||||||
|
secret=secret,
|
||||||
|
username=user or None,
|
||||||
|
password=password or None,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
client.check_connection()
|
||||||
|
except TeleSecCouchDBError as exc:
|
||||||
|
print(f"Error de conexión CouchDB: {exc}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
if args.once:
|
||||||
|
run_once(client=client, machine_id=machine_id, dry_run=args.dry_run)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
run_once(client=client, machine_id=machine_id, dry_run=args.dry_run)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Error en iteración agente: {exc}", file=sys.stderr)
|
||||||
|
time.sleep(max(5, int(interval)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -1 +1,3 @@
|
|||||||
requests
|
requests
|
||||||
|
pycryptodome
|
||||||
|
psutil
|
||||||
@@ -1,43 +1,50 @@
|
|||||||
function fixfloat(number) {
|
function fixfloat(number) {
|
||||||
return (parseFloat(number).toPrecision(8));
|
return parseFloat(number).toPrecision(8);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tableScroll(query) {
|
function tableScroll(query) {
|
||||||
$(query).doubleScroll();
|
$(query).doubleScroll();
|
||||||
}
|
}
|
||||||
//var secretTokenEl = document.getElementById("secretToken");
|
//var secretTokenEl = document.getElementById("secretToken");
|
||||||
var container = document.getElementById("container");
|
var container = document.getElementById('container');
|
||||||
|
|
||||||
function open_page(params) {
|
function open_page(params) {
|
||||||
// Clear stored event listeners and timers
|
// Clear stored event listeners and timers
|
||||||
EventListeners.GunJS = [];
|
EventListeners.GunJS = [];
|
||||||
EventListeners.Timeout.forEach(ev => clearTimeout(ev));
|
EventListeners.Timeout.forEach((ev) => clearTimeout(ev));
|
||||||
EventListeners.Timeout = [];
|
EventListeners.Timeout = [];
|
||||||
EventListeners.Interval.forEach(ev => clearInterval(ev));
|
EventListeners.Interval.forEach((ev) => clearInterval(ev));
|
||||||
EventListeners.Interval = [];
|
EventListeners.Interval = [];
|
||||||
EventListeners.QRScanner.forEach(ev => ev.clear());
|
EventListeners.QRScanner.forEach((ev) => ev.clear());
|
||||||
EventListeners.QRScanner = [];
|
EventListeners.QRScanner = [];
|
||||||
EventListeners.Custom.forEach(ev => ev());
|
EventListeners.Custom.forEach((ev) => ev());
|
||||||
EventListeners.Custom = [];
|
EventListeners.Custom = [];
|
||||||
|
EventListeners.DB.forEach((ev) => DB.unlisten(ev));
|
||||||
|
EventListeners.DB = [];
|
||||||
|
|
||||||
if (SUB_LOGGED_IN != true && params != "login,setup") {
|
if (SUB_LOGGED_IN != true && params != 'login,setup' && !params.startsWith('login,onboarding')) {
|
||||||
PAGES["login"].index();
|
PAGES['login'].index();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (params == "") {
|
if (params == '') {
|
||||||
params = "index";
|
params = 'index';
|
||||||
}
|
}
|
||||||
var path = params.split(",");
|
var path = params.split(',');
|
||||||
var app = path[0];
|
var app = path[0];
|
||||||
|
if (!PAGES[app]) {
|
||||||
|
toastr.error('La app solicitada no existe.');
|
||||||
|
setUrlHash('index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (path[1] == undefined) {
|
if (path[1] == undefined) {
|
||||||
PAGES[app].index();
|
PAGES[app].index();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PAGES[app].edit(path[1]);
|
PAGES[app].edit(path.slice(1).join(','));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setUrlHash(hash) {
|
function setUrlHash(hash) {
|
||||||
location.hash = "#" + hash;
|
location.hash = '#' + hash;
|
||||||
|
|
||||||
// Handle quick search transfer
|
// Handle quick search transfer
|
||||||
if (hash === 'buscar') {
|
if (hash === 'buscar') {
|
||||||
@@ -50,18 +57,18 @@ function setUrlHash(hash) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.onhashchange = () => {
|
window.onhashchange = () => {
|
||||||
open_page(location.hash.replace("#", ""));
|
open_page(location.hash.replace('#', '').split("?")[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
function download(filename, text) {
|
function download(filename, text) {
|
||||||
var element = document.createElement("a");
|
var element = document.createElement('a');
|
||||||
element.setAttribute(
|
element.setAttribute(
|
||||||
"href",
|
'href',
|
||||||
"data:application/octet-stream;charset=utf-8," + encodeURIComponent(text)
|
'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text)
|
||||||
);
|
);
|
||||||
element.setAttribute("download", filename);
|
element.setAttribute('download', filename);
|
||||||
|
|
||||||
element.style.display = "none";
|
element.style.display = 'none';
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
|
|
||||||
element.click();
|
element.click();
|
||||||
@@ -69,12 +76,7 @@ function download(filename, text) {
|
|||||||
document.body.removeChild(element);
|
document.body.removeChild(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeInputImage(
|
function resizeInputImage(file, callback, targetHeight = 256, targetQuality = 0.75) {
|
||||||
file,
|
|
||||||
callback,
|
|
||||||
targetHeight = 256,
|
|
||||||
targetQuality = 0.75
|
|
||||||
) {
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = function (event) {
|
reader.onload = function (event) {
|
||||||
@@ -83,19 +85,19 @@ function resizeInputImage(
|
|||||||
const aspectRatio = img.width / img.height;
|
const aspectRatio = img.width / img.height;
|
||||||
const targetWidth = targetHeight * aspectRatio;
|
const targetWidth = targetHeight * aspectRatio;
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = targetWidth;
|
canvas.width = targetWidth;
|
||||||
canvas.height = targetHeight;
|
canvas.height = targetHeight;
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
ctx.fillStyle = "#ffffff";
|
ctx.fillStyle = '#ffffff';
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
|
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
|
||||||
|
|
||||||
// Get resized image as Blob
|
// Get resized image as Blob
|
||||||
const dataURL = canvas.toDataURL("image/jpeg", targetQuality);
|
const dataURL = canvas.toDataURL('image/jpeg', targetQuality);
|
||||||
callback(dataURL);
|
callback(dataURL);
|
||||||
};
|
};
|
||||||
img.src = event.target.result;
|
img.src = event.target.result;
|
||||||
@@ -105,7 +107,7 @@ function resizeInputImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CurrentISODate() {
|
function CurrentISODate() {
|
||||||
return new Date().toISOString().split("T")[0].replace("T", " ");
|
return new Date().toISOString().split('T')[0].replace('T', ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
function CurrentISOTime() {
|
function CurrentISOTime() {
|
||||||
@@ -113,7 +115,7 @@ function CurrentISOTime() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fixGunLocalStorage() {
|
function fixGunLocalStorage() {
|
||||||
localStorage.removeItem("radata");
|
localStorage.removeItem('radata');
|
||||||
removeCache();
|
removeCache();
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
@@ -125,7 +127,6 @@ function fixGunLocalStorage() {
|
|||||||
// }
|
// }
|
||||||
// }, 5000);
|
// }, 5000);
|
||||||
|
|
||||||
|
|
||||||
function betterSorter(a, b) {
|
function betterSorter(a, b) {
|
||||||
// 1. Fecha (ascending)
|
// 1. Fecha (ascending)
|
||||||
if (a.Fecha && b.Fecha && a.Fecha !== b.Fecha) {
|
if (a.Fecha && b.Fecha && a.Fecha !== b.Fecha) {
|
||||||
@@ -133,29 +134,19 @@ function betterSorter(a, b) {
|
|||||||
}
|
}
|
||||||
// 2. Region (ascending, from SC_Personas if Persona exists)
|
// 2. Region (ascending, from SC_Personas if Persona exists)
|
||||||
const regionA =
|
const regionA =
|
||||||
a.Persona && SC_Personas[a.Persona]
|
a.Persona && SC_Personas[a.Persona] ? SC_Personas[a.Persona].Region || '' : a.Region || '';
|
||||||
? SC_Personas[a.Persona].Region || ""
|
|
||||||
: a.Region || "";
|
|
||||||
const regionB =
|
const regionB =
|
||||||
b.Persona && SC_Personas[b.Persona]
|
b.Persona && SC_Personas[b.Persona] ? SC_Personas[b.Persona].Region || '' : b.Region || '';
|
||||||
? SC_Personas[b.Persona].Region || ""
|
|
||||||
: b.Region || "";
|
|
||||||
if (regionA !== regionB) {
|
if (regionA !== regionB) {
|
||||||
return regionA.toLowerCase() < regionB.toLowerCase() ? -1 : 1;
|
return regionA.toLowerCase() < regionB.toLowerCase() ? -1 : 1;
|
||||||
}
|
}
|
||||||
// 3. Persona (Nombre, ascending, from SC_Personas if Persona exists)
|
// 3. Persona (Nombre, ascending, from SC_Personas if Persona exists)
|
||||||
const nombrePersonaA =
|
const nombrePersonaA =
|
||||||
a.Persona && SC_Personas[a.Persona]
|
a.Persona && SC_Personas[a.Persona] ? SC_Personas[a.Persona].Nombre || '' : '';
|
||||||
? SC_Personas[a.Persona].Nombre || ""
|
|
||||||
: "";
|
|
||||||
const nombrePersonaB =
|
const nombrePersonaB =
|
||||||
b.Persona && SC_Personas[b.Persona]
|
b.Persona && SC_Personas[b.Persona] ? SC_Personas[b.Persona].Nombre || '' : '';
|
||||||
? SC_Personas[b.Persona].Nombre || ""
|
|
||||||
: "";
|
|
||||||
if (nombrePersonaA !== nombrePersonaB) {
|
if (nombrePersonaA !== nombrePersonaB) {
|
||||||
return nombrePersonaA.toLowerCase() < nombrePersonaB.toLowerCase()
|
return nombrePersonaA.toLowerCase() < nombrePersonaB.toLowerCase() ? -1 : 1;
|
||||||
? -1
|
|
||||||
: 1;
|
|
||||||
}
|
}
|
||||||
// 4. Nombre (ascending, from a.Nombre/b.Nombre)
|
// 4. Nombre (ascending, from a.Nombre/b.Nombre)
|
||||||
if (a.Nombre && b.Nombre && a.Nombre !== b.Nombre) {
|
if (a.Nombre && b.Nombre && a.Nombre !== b.Nombre) {
|
||||||
|
|||||||
1911
src/app_modules.js
113
src/config.js
@@ -1,48 +1,112 @@
|
|||||||
|
// 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 = {
|
var EventListeners = {
|
||||||
GunJS: [],
|
GunJS: [],
|
||||||
Timeout: [],
|
Timeout: [],
|
||||||
Interval: [],
|
Interval: [],
|
||||||
QRScanner: [],
|
QRScanner: [],
|
||||||
Custom: [],
|
Custom: [],
|
||||||
|
DB: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
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) {
|
if (!crypto.randomUUID) {
|
||||||
// Fallback for environments without crypto.randomUUID()
|
// Fallback for environments without crypto.randomUUID()
|
||||||
const randomPart = Math.random().toString(36).substring(2, 10);
|
const randomPart = Math.random().toString(36).substring(2, 10);
|
||||||
return prefix + randomPart;
|
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 urlParams = new URLSearchParams(location.search);
|
||||||
var AC_BYPASS = false;
|
var AC_BYPASS = false;
|
||||||
if (urlParams.get("ac_bypass") == "yes") {
|
if (urlParams.get('ac_bypass') == 'yes') {
|
||||||
AC_BYPASS = true;
|
AC_BYPASS = true;
|
||||||
}
|
}
|
||||||
if (urlParams.get("hidenav") != undefined) {
|
if (urlParams.get('hidenav') != undefined) {
|
||||||
document.getElementById("header_hide_query").style.display = "none";
|
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.split("?")[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
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'
|
// getDBName: prefer explicit CouchDB dbname from settings. Single-group model: default to 'telesec'
|
||||||
function getDBName() {
|
function getDBName() {
|
||||||
const dbname = localStorage.getItem('TELESEC_COUCH_DBNAME') || '';
|
const dbname = localStorage.getItem('TELESEC_COUCH_DBNAME') || '';
|
||||||
if (dbname && dbname.trim() !== '') return dbname.trim();
|
if (dbname && dbname.trim() !== '') return dbname.trim();
|
||||||
return 'telesec';
|
return 'telesec';
|
||||||
}
|
}
|
||||||
// const PUBLIC_KEY = "~cppGiuA4UFUPGTDoC-4r2izVC3F7MfpaCmF3iZdESN4.vntmjgbAVUpF_zfinYY6EKVFuuTYxh5xOrL4KmtdTmc"
|
var SECRET = '';
|
||||||
// `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 SUB_LOGGED_IN = false;
|
var SUB_LOGGED_IN = false;
|
||||||
var SUB_LOGGED_IN_DETAILS = false;
|
var SUB_LOGGED_IN_DETAILS = false;
|
||||||
var SUB_LOGGED_IN_ID = false;
|
var SUB_LOGGED_IN_ID = false;
|
||||||
var SAVE_WAIT = 500;
|
var SAVE_WAIT = 500;
|
||||||
var SC_Personas = {};
|
var SC_Personas = {};
|
||||||
var PeerConnectionInterval = 5000;
|
var PeerConnectionInterval = 5000;
|
||||||
if (urlParams.get("sublogin") != null) {
|
if (urlParams.get('sublogin') != null) {
|
||||||
SUB_LOGGED_IN = true;
|
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];
|
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID];
|
||||||
var sli = 15;
|
var sli = 15;
|
||||||
var slii = setInterval(() => {
|
var slii = setInterval(() => {
|
||||||
@@ -53,13 +117,30 @@ if (urlParams.get("sublogin") != null) {
|
|||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
// Logout function for sublogin: clears sublogin state and reloads the page without the sublogin parameter
|
||||||
function LogOutTeleSec() {
|
function LogOutTeleSec() {
|
||||||
SUB_LOGGED_IN = false;
|
SUB_LOGGED_IN = false;
|
||||||
SUB_LOGGED_IN_DETAILS = false;
|
SUB_LOGGED_IN_DETAILS = false;
|
||||||
SUB_LOGGED_IN_ID = false;
|
SUB_LOGGED_IN_ID = false;
|
||||||
document.getElementById("loading").style.display = "block";
|
document.getElementById('loading').style.display = 'block';
|
||||||
//Remove sublogin from URL and reload
|
//Remove sublogin from URL and reload
|
||||||
urlParams.delete("sublogin");
|
urlParams.delete('sublogin');
|
||||||
history.replaceState(null, "", "?" + urlParams.toString());
|
history.replaceState(null, '', '?' + urlParams.toString());
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
var TTS_RATE = parseFloat(urlParams.get('tts_rate')) || 0.75;
|
||||||
|
function TS_SayTTS(msg) {
|
||||||
|
try {
|
||||||
|
if (window.speechSynthesis) {
|
||||||
|
let utterance = new SpeechSynthesisUtterance(msg);
|
||||||
|
utterance.rate = TTS_RATE;
|
||||||
|
speechSynthesis.speak(utterance);
|
||||||
|
}
|
||||||
|
} catch { console.warn('TTS error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function createElementFromHTML(htmlString) {
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.innerHTML = htmlString.trim();
|
||||||
|
return div.firstChild;
|
||||||
|
}
|
||||||
207
src/db.js
@@ -9,7 +9,8 @@ var DB = (function () {
|
|||||||
let changes = null;
|
let changes = null;
|
||||||
let repPush = null;
|
let repPush = null;
|
||||||
let repPull = null;
|
let repPull = null;
|
||||||
let callbacks = {}; // table -> [cb]
|
let callbacks = {}; // table -> [{ id, cb }]
|
||||||
|
let callbackSeq = 0;
|
||||||
let docCache = {}; // _id -> last data snapshot (stringified)
|
let docCache = {}; // _id -> last data snapshot (stringified)
|
||||||
|
|
||||||
function ensureLocal() {
|
function ensureLocal() {
|
||||||
@@ -18,9 +19,13 @@ var DB = (function () {
|
|||||||
const localName = 'telesec';
|
const localName = 'telesec';
|
||||||
local = new PouchDB(localName);
|
local = new PouchDB(localName);
|
||||||
if (changes) {
|
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) {
|
} catch (e) {
|
||||||
console.warn('ensureLocal error', e);
|
console.warn('ensureLocal error', e);
|
||||||
}
|
}
|
||||||
@@ -30,12 +35,19 @@ var DB = (function () {
|
|||||||
return table + ':' + id;
|
return table + ':' + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeCallbackId(table) {
|
||||||
|
callbackSeq += 1;
|
||||||
|
return table + '#' + callbackSeq;
|
||||||
|
}
|
||||||
|
|
||||||
function init(opts) {
|
function init(opts) {
|
||||||
const localName = 'telesec';
|
const localName = 'telesec';
|
||||||
try {
|
try {
|
||||||
if (opts && opts.secret) {
|
if (opts && opts.secret) {
|
||||||
SECRET = opts.secret;
|
SECRET = opts.secret;
|
||||||
try { localStorage.setItem('TELESEC_SECRET', SECRET); } catch (e) {}
|
try {
|
||||||
|
localStorage.setItem('TELESEC_SECRET', SECRET);
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
local = new PouchDB(localName);
|
local = new PouchDB(localName);
|
||||||
@@ -43,7 +55,7 @@ var DB = (function () {
|
|||||||
if (opts.remoteServer) {
|
if (opts.remoteServer) {
|
||||||
try {
|
try {
|
||||||
const server = opts.remoteServer.replace(/\/$/, '');
|
const server = opts.remoteServer.replace(/\/$/, '');
|
||||||
const dbname = encodeURIComponent((opts.dbname || localName));
|
const dbname = encodeURIComponent(opts.dbname || localName);
|
||||||
let authPart = '';
|
let authPart = '';
|
||||||
if (opts.username) authPart = opts.username + ':' + (opts.password || '') + '@';
|
if (opts.username) authPart = opts.username + ':' + (opts.password || '') + '@';
|
||||||
const remoteUrl = server.replace(/https?:\/\//, (m) => m) + '/' + dbname;
|
const remoteUrl = server.replace(/https?:\/\//, (m) => m) + '/' + dbname;
|
||||||
@@ -56,39 +68,77 @@ var DB = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (changes) changes.cancel();
|
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();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
function replicateToRemote() {
|
function replicateToRemote() {
|
||||||
ensureLocal();
|
ensureLocal();
|
||||||
if (!local || !remote) return;
|
if (!local || !remote) return;
|
||||||
try { if (repPush && repPush.cancel) repPush.cancel(); } catch (e) {}
|
try {
|
||||||
try { if (repPull && repPull.cancel) repPull.cancel(); } catch (e) {}
|
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 })
|
repPush = PouchDB.replicate(local, remote, { live: true, retry: true }).on(
|
||||||
.on('error', function (err) { console.warn('Replication push error', err); });
|
'error',
|
||||||
repPull = PouchDB.replicate(remote, local, { live: true, retry: true })
|
function (err) {
|
||||||
.on('error', function (err) { console.warn('Replication pull error', 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) {
|
if (typeof window !== 'undefined' && window.addEventListener) {
|
||||||
window.addEventListener('online', function () {
|
window.addEventListener('online', function () {
|
||||||
try { setTimeout(replicateToRemote, 1000); } catch (e) {}
|
try {
|
||||||
|
setTimeout(replicateToRemote, 1000);
|
||||||
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChange(change) {
|
function onChange(change) {
|
||||||
const doc = change.doc;
|
const doc = change.doc;
|
||||||
if (!doc || !doc._id) return;
|
if (!doc || !doc._id) return;
|
||||||
|
try {
|
||||||
|
window.TELESEC_LAST_SYNC = Date.now();
|
||||||
|
// 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 || {});
|
||||||
|
} catch (e) {
|
||||||
|
payload = String(doc._id || '');
|
||||||
|
}
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < payload.length; i++) {
|
||||||
|
hash = (hash * 31 + payload.charCodeAt(i)) >>> 0;
|
||||||
|
}
|
||||||
|
const hue = hash % 360;
|
||||||
|
window.TELESEC_LAST_SYNC_COLOR = `hsl(${hue}, 70%, 50%)`;
|
||||||
|
updateStatusOrb();
|
||||||
|
} catch (e) {}
|
||||||
const [table, id] = doc._id.split(':');
|
const [table, id] = doc._id.split(':');
|
||||||
|
|
||||||
// handle deletes
|
// handle deletes
|
||||||
if (change.deleted || doc._deleted) {
|
if (change.deleted || doc._deleted) {
|
||||||
delete docCache[doc._id];
|
delete docCache[doc._id];
|
||||||
if (callbacks[table]) {
|
if (callbacks[table]) {
|
||||||
callbacks[table].forEach(cb => {
|
callbacks[table].forEach((listener) => {
|
||||||
try { cb(null, id); } catch (e) { console.error(e); }
|
const cb = listener.cb;
|
||||||
|
try {
|
||||||
|
cb(null, id);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -100,11 +150,18 @@ var DB = (function () {
|
|||||||
const prev = docCache[doc._id];
|
const prev = docCache[doc._id];
|
||||||
if (prev === now) return; // no meaningful change
|
if (prev === now) return; // no meaningful change
|
||||||
docCache[doc._id] = now;
|
docCache[doc._id] = now;
|
||||||
} catch (e) { /* ignore cache errors */ }
|
} catch (e) {
|
||||||
|
/* ignore cache errors */
|
||||||
|
}
|
||||||
|
|
||||||
if (callbacks[table]) {
|
if (callbacks[table]) {
|
||||||
callbacks[table].forEach(cb => {
|
callbacks[table].forEach((listener) => {
|
||||||
try { cb(doc.data, id); } catch (e) { console.error(e); }
|
const cb = listener.cb;
|
||||||
|
try {
|
||||||
|
cb(doc.data, id);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,13 +178,25 @@ var DB = (function () {
|
|||||||
const doc = existing || { _id: _id };
|
const doc = existing || { _id: _id };
|
||||||
var toStore = data;
|
var toStore = data;
|
||||||
try {
|
try {
|
||||||
var isEncryptedString = (typeof data === 'string' && data.startsWith('RSA{') && data.endsWith('}'));
|
var isEncryptedString =
|
||||||
if (!isEncryptedString && typeof TS_encrypt === 'function' && typeof SECRET !== 'undefined' && SECRET) {
|
typeof data === 'string' && data.startsWith('RSA{') && data.endsWith('}');
|
||||||
toStore = await new Promise(resolve => {
|
if (
|
||||||
try { TS_encrypt(data, SECRET, enc => resolve(enc)); } catch (e) { resolve(data); }
|
!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.data = toStore;
|
||||||
doc.table = table;
|
doc.table = table;
|
||||||
doc.ts = new Date().toISOString();
|
doc.ts = new Date().toISOString();
|
||||||
@@ -137,7 +206,6 @@ var DB = (function () {
|
|||||||
// FIX: manually trigger map() callbacks for local update
|
// FIX: manually trigger map() callbacks for local update
|
||||||
// onChange will update docCache and notify all subscribers
|
// onChange will update docCache and notify all subscribers
|
||||||
onChange({ doc: doc });
|
onChange({ doc: doc });
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('DB.put error', e);
|
console.error('DB.put error', e);
|
||||||
}
|
}
|
||||||
@@ -149,21 +217,33 @@ var DB = (function () {
|
|||||||
try {
|
try {
|
||||||
const doc = await local.get(_id);
|
const doc = await local.get(_id);
|
||||||
return doc.data;
|
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) {
|
async function list(table) {
|
||||||
ensureLocal();
|
ensureLocal();
|
||||||
try {
|
try {
|
||||||
const res = await local.allDocs({ include_docs: true, startkey: table + ':', endkey: table + ':\uffff' });
|
const res = await local.allDocs({
|
||||||
return res.rows.map(r => {
|
include_docs: true,
|
||||||
|
startkey: table + ':',
|
||||||
|
endkey: table + ':\uffff',
|
||||||
|
});
|
||||||
|
return res.rows.map((r) => {
|
||||||
const id = r.id.split(':')[1];
|
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 };
|
return { id: id, data: r.doc.data };
|
||||||
});
|
});
|
||||||
} catch (e) { return []; }
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dataURLtoBlob(dataurl) {
|
function dataURLtoBlob(dataurl) {
|
||||||
@@ -186,11 +266,15 @@ var DB = (function () {
|
|||||||
doc = await local.get(_id);
|
doc = await local.get(_id);
|
||||||
}
|
}
|
||||||
let blob = dataUrlOrBlob;
|
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';
|
const type = contentType || (blob && blob.type) || 'application/octet-stream';
|
||||||
await local.putAttachment(_id, name, doc._rev, blob, type);
|
await local.putAttachment(_id, name, doc._rev, blob, type);
|
||||||
return true;
|
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) {
|
async function getAttachment(table, id, name) {
|
||||||
@@ -201,11 +285,13 @@ var DB = (function () {
|
|||||||
if (!blob) return null;
|
if (!blob) return null;
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = e => resolve(e.target.result);
|
reader.onload = (e) => resolve(e.target.result);
|
||||||
reader.onerror = e => reject(e);
|
reader.onerror = (e) => reject(e);
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
});
|
});
|
||||||
} catch (e) { return null; }
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listAttachments(table, id) {
|
async function listAttachments(table, id) {
|
||||||
@@ -242,10 +328,14 @@ var DB = (function () {
|
|||||||
try {
|
try {
|
||||||
const durl = await getAttachment(table, id, name);
|
const durl = await getAttachment(table, id, name);
|
||||||
out.push({ name: name, dataUrl: durl, content_type: null });
|
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;
|
return out;
|
||||||
} catch (e2) { return []; }
|
} catch (e2) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,15 +348,33 @@ var DB = (function () {
|
|||||||
delete doc._attachments[name];
|
delete doc._attachments[name];
|
||||||
await local.put(doc);
|
await local.put(doc);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) { console.error('deleteAttachment error', e); return false; }
|
} catch (e) {
|
||||||
|
console.error('deleteAttachment error', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function map(table, cb) {
|
function map(table, cb) {
|
||||||
ensureLocal();
|
ensureLocal();
|
||||||
|
const callbackId = makeCallbackId(table);
|
||||||
callbacks[table] = callbacks[table] || [];
|
callbacks[table] = callbacks[table] || [];
|
||||||
callbacks[table].push(cb);
|
callbacks[table].push({ id: callbackId, cb: cb });
|
||||||
list(table).then(rows => rows.forEach(r => cb(r.data, r.id)));
|
list(table).then((rows) => {
|
||||||
return () => { callbacks[table] = callbacks[table].filter(x => x !== cb); }
|
const stillListening = (callbacks[table] || []).some((listener) => listener.id === callbackId);
|
||||||
|
if (!stillListening) return;
|
||||||
|
rows.forEach((r) => cb(r.data, r.id));
|
||||||
|
});
|
||||||
|
return callbackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlisten(callbackId) {
|
||||||
|
if (!callbackId) return false;
|
||||||
|
for (const table of Object.keys(callbacks)) {
|
||||||
|
const before = callbacks[table].length;
|
||||||
|
callbacks[table] = callbacks[table].filter((listener) => listener.id !== callbackId);
|
||||||
|
if (callbacks[table].length !== before) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -276,12 +384,13 @@ var DB = (function () {
|
|||||||
del,
|
del,
|
||||||
list,
|
list,
|
||||||
map,
|
map,
|
||||||
|
unlisten,
|
||||||
replicateToRemote,
|
replicateToRemote,
|
||||||
listAttachments,
|
listAttachments,
|
||||||
deleteAttachment,
|
deleteAttachment,
|
||||||
putAttachment,
|
putAttachment,
|
||||||
getAttachment,
|
getAttachment,
|
||||||
_internal: { local }
|
_internal: { local },
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -294,7 +403,15 @@ window.DB = DB;
|
|||||||
const username = localStorage.getItem('TELESEC_COUCH_USER') || '';
|
const username = localStorage.getItem('TELESEC_COUCH_USER') || '';
|
||||||
const password = localStorage.getItem('TELESEC_COUCH_PASS') || '';
|
const password = localStorage.getItem('TELESEC_COUCH_PASS') || '';
|
||||||
const dbname = localStorage.getItem('TELESEC_COUCH_DBNAME') || undefined;
|
const dbname = localStorage.getItem('TELESEC_COUCH_DBNAME') || undefined;
|
||||||
try { SECRET = localStorage.getItem('TELESEC_SECRET') || ''; } catch (e) { SECRET = ''; }
|
try {
|
||||||
DB.init({ remoteServer, username, password, dbname }).catch(e => console.warn('DB.autoInit error', e));
|
SECRET = localStorage.getItem('TELESEC_SECRET') || '';
|
||||||
} catch (e) { console.warn('DB.autoInit unexpected error', e); }
|
} 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);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="ribbon no_print" id="header_hide_query">
|
<div class="ribbon no_print" id="header_hide_query">
|
||||||
<img class="ribbon-orb" id="connectStatus" src="static/logo.jpg" />
|
<img class="ribbon-orb" id="connectStatus" src="static/logo.jpg" style="cursor: pointer;" />
|
||||||
|
|
||||||
<div class="ribbon-content">
|
<div class="ribbon-content" id="ribbon-content" style="display: none;">
|
||||||
<div class="ribbon-tabs">
|
<div class="ribbon-tabs">
|
||||||
<div class="ribbon-tab active" data-tab="modulos">Modulos</div>
|
<div class="ribbon-tab active" data-tab="modulos">Modulos</div>
|
||||||
<div class="ribbon-tab" data-tab="buscar">Buscar</div>
|
<div class="ribbon-tab" data-tab="buscar">Buscar</div>
|
||||||
@@ -43,17 +43,13 @@
|
|||||||
|
|
||||||
<small style="margin-top:10px;">Base de datos: <b id="peerLink">(no configurado)</b></small>
|
<small style="margin-top:10px;">Base de datos: <b id="peerLink">(no configurado)</b></small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ribbon-content" id="ribbon-content-alternative">
|
||||||
|
<h2 style="margin: 0;">TeleSec, muy seguro. </h2>
|
||||||
|
<i><small>(pulsa el icono para mostrar el menú)</small></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img id="loading" src="load.gif" style="display: block; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: calc(100% - 50px); max-width: 400px;" />
|
<img id="loading" src="load.gif" style="display: block; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: calc(100% - 50px); max-width: 400px;" />
|
||||||
<details class="supermesh-indicator">
|
|
||||||
<summary>
|
|
||||||
<b>Sincronización</b><br />
|
|
||||||
<br /><small id="peerPID" style="font-family: monospace">Estado: local</small>
|
|
||||||
</summary>
|
|
||||||
<ul id="peerList"></ul>
|
|
||||||
<i>Todos los datos están encriptados.</i>
|
|
||||||
</details>
|
|
||||||
<main id="container"></main>
|
<main id="container"></main>
|
||||||
<img id="actionStatus" src="static/ico/statusok.png" style="
|
<img id="actionStatus" src="static/ico/statusok.png" style="
|
||||||
z-index: 2048;
|
z-index: 2048;
|
||||||
@@ -69,17 +65,16 @@
|
|||||||
<div id="snackbar">
|
<div id="snackbar">
|
||||||
Hay una nueva versión de %%TITLE%%.<br /><a id="reload">Pulsa aqui para actualizar.</a>
|
Hay una nueva versión de %%TITLE%%.<br /><a id="reload">Pulsa aqui para actualizar.</a>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js" integrity="sha256-/H4YS+7aYb9kJ5OKhFYPUjSJdrtV6AeyJOtTkw6X72o=" crossorigin="anonymous"></script>
|
<script src="static/aes.js"></script>
|
||||||
<script src="static/showdown.min.js"></script>
|
<script src="static/showdown.min.js"></script>
|
||||||
<script src="static/qrcode/html5-qrcode.min.js"></script>
|
<script src="static/qrcode/html5-qrcode.min.js"></script>
|
||||||
<script src="static/qrcode/barcode.js"></script>
|
<script src="static/qrcode/barcode.js"></script>
|
||||||
|
<script src="static/qrcode/qrcode.min.js"></script>
|
||||||
<script src="static/jquery.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/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/toastr.min.js"></script>
|
||||||
<script src="static/doublescroll.js"></script>
|
<script src="static/doublescroll.js"></script>
|
||||||
<!--<script src="static/simplemde.min.js"></script>-->
|
<script src="static/chart.umd.min.js"></script>
|
||||||
<script src="pwa.js"></script>
|
<script src="pwa.js"></script>
|
||||||
<script src="config.js"></script>
|
<script src="config.js"></script>
|
||||||
<script src="db.js"></script>
|
<script src="db.js"></script>
|
||||||
@@ -96,6 +91,8 @@
|
|||||||
<!-- <script src="page/avisos.js"></script> -->
|
<!-- <script src="page/avisos.js"></script> -->
|
||||||
<script src="page/comedor.js"></script>
|
<script src="page/comedor.js"></script>
|
||||||
<script src="page/notas.js"></script>
|
<script src="page/notas.js"></script>
|
||||||
|
<script src="page/mensajes.js"></script>
|
||||||
|
<script src="page/panel.js"></script>
|
||||||
<!-- <script src="page/chat.js"></script> -->
|
<!-- <script src="page/chat.js"></script> -->
|
||||||
<script src="page/buscar.js"></script>
|
<script src="page/buscar.js"></script>
|
||||||
<script src="page/pagos.js"></script>
|
<script src="page/pagos.js"></script>
|
||||||
|
|||||||
@@ -1,63 +1,108 @@
|
|||||||
PERMS["aulas"] = "Aulas (Solo docentes!)";
|
PERMS['aulas'] = 'Aulas (Solo docentes!)';
|
||||||
PAGES.aulas = {
|
PAGES.aulas = {
|
||||||
//navcss: "btn1",
|
//navcss: "btn1",
|
||||||
Title: "Gest-Aula",
|
Title: 'Gest-Aula',
|
||||||
icon: "static/appico/components.png",
|
icon: 'static/appico/components.png',
|
||||||
AccessControl: true,
|
AccessControl: true,
|
||||||
index: function () {
|
index: function () {
|
||||||
if (!checkRole("aulas")) {
|
if (!checkRole('aulas')) {
|
||||||
setUrlHash("index");
|
setUrlHash('index');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var data_Comedor = safeuuid();
|
var data_Comedor = safeuuid();
|
||||||
var data_Tareas = safeuuid();
|
var data_Tareas = safeuuid();
|
||||||
var data_Diario = safeuuid();
|
var data_Diario = safeuuid();
|
||||||
var data_Weather = safeuuid();
|
var data_Weather = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Gestión del Aula</h1>
|
<h1>Gestión del Aula</h1>
|
||||||
<div>
|
<div>
|
||||||
<fieldset style="float: left;">
|
<fieldset style="float: left;">
|
||||||
<legend><img src="${PAGES.notas.icon}" height="20"> Notas esenciales</legend>
|
<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,inicio_dia"
|
||||||
<a class="button" style="font-size: 25px;" href="#notas,realizacion_cafe">Como realizar el café</a>
|
>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,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,horario">Horario</a>
|
||||||
<a class="button" style="font-size: 25px;" href="#notas,tareas">Tareas</a>
|
<a class="button" style="font-size: 25px;" href="#notas,tareas">Tareas</a>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset style="float: left;">
|
<fieldset style="float: left;">
|
||||||
<legend>Acciones</legend>
|
<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,solicitudes"
|
||||||
<a class="button" style="font-size: 25px;" href="#aulas,informes,diario-${CurrentISODate()}">Diario de hoy</a>
|
><img src="${PAGES.materiales.icon}" height="20" /> Solicitudes de material</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
|
||||||
<a class="button btn4" style="font-size: 25px;" href="#supercafe"><img src="${PAGES.supercafe.icon}" height="20"> Ver comandas</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
|
||||||
|
>
|
||||||
|
<a class="button btn8" style="font-size: 25px;" href="#aulas,ordenadores"
|
||||||
|
><img src="${PAGES.aulas.icon}" height="20" /> Control de ordenadores</a
|
||||||
|
>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset style="float: left;">
|
<fieldset style="float: left;">
|
||||||
<legend>Datos de hoy</legend>
|
<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
|
||||||
<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>
|
class="btn7"
|
||||||
<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>
|
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"
|
||||||
<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>
|
><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>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
//#region Cargar Clima
|
//#region Cargar Clima
|
||||||
// Get location from DB settings.weather_location; if missing ask user and save it
|
// Get location from DB settings.weather_location; if missing ask user and save it
|
||||||
// url format: https://wttr.in/<loc>?F0m
|
// url format: https://wttr.in/<loc>?F0m
|
||||||
DB.get('settings', 'weather_location').then((loc) => {
|
DB.get('settings', 'weather_location').then((loc) => {
|
||||||
if (!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) {
|
if (loc) {
|
||||||
DB.put('settings', 'weather_location', loc);
|
DB.put('settings', 'weather_location', loc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (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 {
|
} 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
|
//#endregion Cargar Clima
|
||||||
@@ -65,17 +110,20 @@ PAGES.aulas = {
|
|||||||
DB.get('comedor', CurrentISODate()).then((data) => {
|
DB.get('comedor', CurrentISODate()).then((data) => {
|
||||||
function add_row(data) {
|
function add_row(data) {
|
||||||
// Fix newlines
|
// Fix newlines
|
||||||
data.Platos = data.Platos || "No hay platos registrados para hoy.";
|
data.Platos = data.Platos || 'No hay platos registrados para hoy.';
|
||||||
// Display platos
|
// Display platos
|
||||||
document.getElementById(data_Comedor).innerHTML = data.Platos.replace(
|
document.getElementById(data_Comedor).innerHTML = data.Platos.replace(/\n/g, '<br>');
|
||||||
/\n/g,
|
|
||||||
"<br>"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
TS_decrypt(
|
||||||
|
data,
|
||||||
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
add_row(data || {});
|
add_row(data || {});
|
||||||
}, 'comedor', CurrentISODate());
|
},
|
||||||
|
'comedor',
|
||||||
|
CurrentISODate()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
add_row(data || {});
|
add_row(data || {});
|
||||||
}
|
}
|
||||||
@@ -85,17 +133,20 @@ PAGES.aulas = {
|
|||||||
DB.get('notas', 'tareas').then((data) => {
|
DB.get('notas', 'tareas').then((data) => {
|
||||||
function add_row(data) {
|
function add_row(data) {
|
||||||
// Fix newlines
|
// Fix newlines
|
||||||
data.Contenido = data.Contenido || "No hay tareas.";
|
data.Contenido = data.Contenido || 'No hay tareas.';
|
||||||
// Display platos
|
// Display platos
|
||||||
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(
|
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(/\n/g, '<br>');
|
||||||
/\n/g,
|
|
||||||
"<br>"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
TS_decrypt(
|
||||||
|
data,
|
||||||
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
add_row(data || {});
|
add_row(data || {});
|
||||||
}, 'notas', 'tareas');
|
},
|
||||||
|
'notas',
|
||||||
|
'tareas'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
add_row(data || {});
|
add_row(data || {});
|
||||||
}
|
}
|
||||||
@@ -105,17 +156,20 @@ PAGES.aulas = {
|
|||||||
DB.get('aulas_informes', 'diario-' + CurrentISODate()).then((data) => {
|
DB.get('aulas_informes', 'diario-' + CurrentISODate()).then((data) => {
|
||||||
function add_row(data) {
|
function add_row(data) {
|
||||||
// Fix newlines
|
// Fix newlines
|
||||||
data.Contenido = data.Contenido || "No hay un diario.";
|
data.Contenido = data.Contenido || 'No hay un diario.';
|
||||||
// Display platos
|
// Display platos
|
||||||
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(
|
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(/\n/g, '<br>');
|
||||||
/\n/g,
|
|
||||||
"<br>"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
TS_decrypt(
|
||||||
|
data,
|
||||||
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
add_row(data || {});
|
add_row(data || {});
|
||||||
}, 'aulas_informes', 'diario-' + CurrentISODate());
|
},
|
||||||
|
'aulas_informes',
|
||||||
|
'diario-' + CurrentISODate()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
add_row(data || {});
|
add_row(data || {});
|
||||||
}
|
}
|
||||||
@@ -125,33 +179,33 @@ PAGES.aulas = {
|
|||||||
_solicitudes: function () {
|
_solicitudes: function () {
|
||||||
const tablebody = safeuuid();
|
const tablebody = safeuuid();
|
||||||
var btn_new = safeuuid();
|
var btn_new = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<a class="button" href="#aulas">← Volver a Gestión de Aulas</a>
|
<a class="button" href="#aulas">← Volver a Gestión de Aulas</a>
|
||||||
<h1>Solicitudes de Material</h1>
|
<h1>Solicitudes de Material</h1>
|
||||||
<button id="${btn_new}">Nueva solicitud</button>
|
<button id="${btn_new}">Nueva solicitud</button>
|
||||||
<div id="cont"></div>
|
<div id="cont"></div>
|
||||||
`;
|
`;
|
||||||
TS_IndexElement(
|
TS_IndexElement(
|
||||||
"aulas,solicitudes",
|
'aulas,solicitudes',
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: "Solicitante",
|
key: 'Solicitante',
|
||||||
type: "persona",
|
type: 'persona',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Solicitante",
|
label: 'Solicitante',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Asunto",
|
key: 'Asunto',
|
||||||
type: "raw",
|
type: 'raw',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Asunto",
|
label: 'Asunto',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"aulas_solicitudes",
|
'aulas_solicitudes',
|
||||||
document.querySelector("#cont")
|
document.querySelector('#cont')
|
||||||
);
|
);
|
||||||
document.getElementById(btn_new).onclick = () => {
|
document.getElementById(btn_new).onclick = () => {
|
||||||
setUrlHash("aulas,solicitudes," + safeuuid(""));
|
setUrlHash('aulas,solicitudes,' + safeuuid(''));
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
_solicitudes__edit: function (mid) {
|
_solicitudes__edit: function (mid) {
|
||||||
@@ -161,39 +215,46 @@ PAGES.aulas = {
|
|||||||
var field_autor = safeuuid();
|
var field_autor = safeuuid();
|
||||||
var btn_guardar = safeuuid();
|
var btn_guardar = safeuuid();
|
||||||
var btn_borrar = safeuuid();
|
var btn_borrar = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<a class="button" href="#aulas,solicitudes">← Volver a solicitudes</a>
|
<a class="button" href="#aulas,solicitudes">← Volver a solicitudes</a>
|
||||||
<h1>Solicitud <code id="${nameh1}"></code></h1>
|
<h1>Solicitud <code id="${nameh1}"></code></h1>
|
||||||
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
|
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
|
||||||
<legend>Valores</legend>
|
<legend>Valores</legend>
|
||||||
<div style="max-width: 400px;">
|
<div style="max-width: 400px;">
|
||||||
<label>
|
<label>
|
||||||
Asunto<br>
|
Asunto<br />
|
||||||
<input type="text" id="${field_asunto}" value=""><br><br>
|
<input type="text" id="${field_asunto}" value="" /><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<input type="hidden" id="${field_autor}" readonly value="">
|
<input type="hidden" id="${field_autor}" readonly value="" />
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
Contenido - ¡Incluye el material a solicitar!<br>
|
Contenido - ¡Incluye el material a solicitar!<br />
|
||||||
<textarea id="${field_contenido}" style="width: 100%; height: 400px;"></textarea><br><br>
|
<textarea id="${field_contenido}" style="width: 100%; height: 400px;"></textarea
|
||||||
|
><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<hr>
|
<hr />
|
||||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
(async () => {
|
(async () => {
|
||||||
const data = await DB.get('aulas_solicitudes', mid);
|
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(nameh1).innerText = mid;
|
||||||
document.getElementById(field_asunto).value = data["Asunto"] || "";
|
document.getElementById(field_asunto).value = data['Asunto'] || '';
|
||||||
document.getElementById(field_contenido).value = data["Contenido"] || "";
|
document.getElementById(field_contenido).value = data['Contenido'] || '';
|
||||||
document.getElementById(field_autor).value = data["Solicitante"] || SUB_LOGGED_IN_ID || "";
|
document.getElementById(field_autor).value = data['Solicitante'] || SUB_LOGGED_IN_ID || '';
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
TS_decrypt(
|
||||||
load_data(data, "%E");
|
data,
|
||||||
}, 'aulas_solicitudes', mid);
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
|
load_data(data, '%E');
|
||||||
|
},
|
||||||
|
'aulas_solicitudes',
|
||||||
|
mid
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
load_data(data || {});
|
load_data(data || {});
|
||||||
}
|
}
|
||||||
@@ -204,34 +265,36 @@ PAGES.aulas = {
|
|||||||
if (guardarBtn.disabled) return;
|
if (guardarBtn.disabled) return;
|
||||||
|
|
||||||
guardarBtn.disabled = true;
|
guardarBtn.disabled = true;
|
||||||
guardarBtn.style.opacity = "0.5";
|
guardarBtn.style.opacity = '0.5';
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
Solicitante: document.getElementById(field_autor).value,
|
Solicitante: document.getElementById(field_autor).value,
|
||||||
Contenido: document.getElementById(field_contenido).value,
|
Contenido: document.getElementById(field_contenido).value,
|
||||||
Asunto: document.getElementById(field_asunto).value,
|
Asunto: document.getElementById(field_asunto).value,
|
||||||
};
|
};
|
||||||
document.getElementById("actionStatus").style.display = "block";
|
document.getElementById('actionStatus').style.display = 'block';
|
||||||
DB.put('aulas_solicitudes', mid, data).then(() => {
|
DB.put('aulas_solicitudes', mid, data)
|
||||||
toastr.success("Guardado!");
|
.then(() => {
|
||||||
|
toastr.success('Guardado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
setUrlHash("aulas,solicitudes");
|
setUrlHash('aulas,solicitudes');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
console.warn('DB.put error', e);
|
console.warn('DB.put error', e);
|
||||||
guardarBtn.disabled = false;
|
guardarBtn.disabled = false;
|
||||||
guardarBtn.style.opacity = "1";
|
guardarBtn.style.opacity = '1';
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
toastr.error("Error al guardar la solicitud");
|
toastr.error('Error al guardar la solicitud');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
document.getElementById(btn_borrar).onclick = () => {
|
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(() => {
|
DB.del('aulas_solicitudes', mid).then(() => {
|
||||||
toastr.error("Borrado!");
|
toastr.error('Borrado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setUrlHash("aulas,solicitudes");
|
setUrlHash('aulas,solicitudes');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -242,53 +305,56 @@ PAGES.aulas = {
|
|||||||
var btn_new = safeuuid();
|
var btn_new = safeuuid();
|
||||||
var field_new_byday = safeuuid();
|
var field_new_byday = safeuuid();
|
||||||
var btn_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>
|
<a class="button" href="#aulas">← Volver a Gestión de Aulas</a>
|
||||||
<h1>Informes</h1>
|
<h1>Informes</h1>
|
||||||
<div style="display: inline-block; border: 2px solid black; padding: 5px; border-radius: 5px;">
|
<div
|
||||||
<b>Diario:</b><br>
|
style="display: inline-block; border: 2px solid black; padding: 5px; border-radius: 5px;"
|
||||||
<input type="date" id="${field_new_byday}" value="${CurrentISODate()}">
|
>
|
||||||
|
<b>Diario:</b><br />
|
||||||
|
<input type="date" id="${field_new_byday}" value="${CurrentISODate()}" />
|
||||||
<button id="${btn_new_byday}">Abrir / Nuevo</button>
|
<button id="${btn_new_byday}">Abrir / Nuevo</button>
|
||||||
</div><br>
|
</div>
|
||||||
|
<br />
|
||||||
<button id="${btn_new}">Nuevo informe</button>
|
<button id="${btn_new}">Nuevo informe</button>
|
||||||
<div id="cont"></div>
|
<div id="cont"></div>
|
||||||
`;
|
`;
|
||||||
TS_IndexElement(
|
TS_IndexElement(
|
||||||
"aulas,informes",
|
'aulas,informes',
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: "Autor",
|
key: 'Autor',
|
||||||
type: "persona",
|
type: 'persona',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Autor",
|
label: 'Autor',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Fecha",
|
key: 'Fecha',
|
||||||
type: "fecha",
|
type: 'fecha',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Fecha",
|
label: 'Fecha',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Asunto",
|
key: 'Asunto',
|
||||||
type: "raw",
|
type: 'raw',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Asunto",
|
label: 'Asunto',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"aulas_informes",
|
'aulas_informes',
|
||||||
document.querySelector("#cont")
|
document.querySelector('#cont')
|
||||||
);
|
);
|
||||||
document.getElementById(btn_new).onclick = () => {
|
document.getElementById(btn_new).onclick = () => {
|
||||||
setUrlHash("aulas,informes," + safeuuid(""));
|
setUrlHash('aulas,informes,' + safeuuid(''));
|
||||||
};
|
};
|
||||||
document.getElementById(btn_new_byday).onclick = () => {
|
document.getElementById(btn_new_byday).onclick = () => {
|
||||||
const day = document.getElementById(field_new_byday).value;
|
const day = document.getElementById(field_new_byday).value;
|
||||||
if (day) {
|
if (day) {
|
||||||
setUrlHash("aulas,informes,diario-" + day);
|
setUrlHash('aulas,informes,diario-' + day);
|
||||||
} else {
|
} else {
|
||||||
toastr.error("Selecciona un día válido");
|
toastr.error('Selecciona un día válido');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
},
|
},
|
||||||
_informes__edit: function (mid) {
|
_informes__edit: function (mid) {
|
||||||
var nameh1 = safeuuid();
|
var nameh1 = safeuuid();
|
||||||
@@ -298,45 +364,49 @@ PAGES.aulas = {
|
|||||||
var field_fecha = safeuuid();
|
var field_fecha = safeuuid();
|
||||||
var btn_guardar = safeuuid();
|
var btn_guardar = safeuuid();
|
||||||
var btn_borrar = safeuuid();
|
var btn_borrar = safeuuid();
|
||||||
var title = "";
|
var title = '';
|
||||||
if (mid.startsWith("diario-")) {
|
if (mid.startsWith('diario-')) {
|
||||||
var date = mid.replace("diario-", "").split("-");
|
var date = mid.replace('diario-', '').split('-');
|
||||||
title = "Diario " + date[2] + "/" + date[1] + "/" + date[0];
|
title = 'Diario ' + date[2] + '/' + date[1] + '/' + date[0];
|
||||||
}
|
}
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<a class="button" href="#aulas,informes">← Volver a informes</a>
|
<a class="button" href="#aulas,informes">← Volver a informes</a>
|
||||||
<h1>Informe <code id="${nameh1}"></code></h1>
|
<h1>Informe <code id="${nameh1}"></code></h1>
|
||||||
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
|
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
|
||||||
<legend>Valores</legend>
|
<legend>Valores</legend>
|
||||||
<div style="max-width: 400px;">
|
<div style="max-width: 400px;">
|
||||||
<label>
|
<label>
|
||||||
Asunto<br>
|
Asunto<br />
|
||||||
<input type="text" id="${field_asunto}" value=""><br><br>
|
<input type="text" id="${field_asunto}" value="" /><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<input type="hidden" id="${field_autor}" readonly value="">
|
<input type="hidden" id="${field_autor}" readonly value="" />
|
||||||
<input type="hidden" id="${field_fecha}" value="">
|
<input type="hidden" id="${field_fecha}" value="" />
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
Contenido<br>
|
Contenido<br />
|
||||||
<textarea id="${field_contenido}" style="width: 100%; height: 400px;"></textarea><br><br>
|
<textarea id="${field_contenido}" style="width: 100%; height: 400px;"></textarea
|
||||||
|
><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<hr>
|
<hr />
|
||||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
(async () => {
|
(async () => {
|
||||||
const data = await DB.get('aulas_informes', mid);
|
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(nameh1).innerText = mid;
|
||||||
document.getElementById(field_asunto).value = data["Asunto"] || title || "";
|
document.getElementById(field_asunto).value = data['Asunto'] || title || '';
|
||||||
document.getElementById(field_contenido).value = data["Contenido"] || "";
|
document.getElementById(field_contenido).value = data['Contenido'] || '';
|
||||||
document.getElementById(field_autor).value = data["Autor"] || SUB_LOGGED_IN_ID || "";
|
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_fecha).value =
|
||||||
|
data['Fecha'] || mid.startsWith('diario-')
|
||||||
|
? mid.replace('diario-', '')
|
||||||
|
: CurrentISODate();
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data) => {
|
TS_decrypt(data, SECRET, (data) => {
|
||||||
load_data(data, "%E");
|
load_data(data, '%E');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
load_data(data || {});
|
load_data(data || {});
|
||||||
@@ -348,7 +418,7 @@ PAGES.aulas = {
|
|||||||
if (guardarBtn.disabled) return;
|
if (guardarBtn.disabled) return;
|
||||||
|
|
||||||
guardarBtn.disabled = true;
|
guardarBtn.disabled = true;
|
||||||
guardarBtn.style.opacity = "0.5";
|
guardarBtn.style.opacity = '0.5';
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
Autor: document.getElementById(field_autor).value,
|
Autor: document.getElementById(field_autor).value,
|
||||||
@@ -356,47 +426,258 @@ PAGES.aulas = {
|
|||||||
Asunto: document.getElementById(field_asunto).value,
|
Asunto: document.getElementById(field_asunto).value,
|
||||||
Fecha: document.getElementById(field_fecha).value || CurrentISODate(),
|
Fecha: document.getElementById(field_fecha).value || CurrentISODate(),
|
||||||
};
|
};
|
||||||
document.getElementById("actionStatus").style.display = "block";
|
document.getElementById('actionStatus').style.display = 'block';
|
||||||
DB.put('aulas_informes', mid, data).then(() => {
|
DB.put('aulas_informes', mid, data)
|
||||||
toastr.success("Guardado!");
|
.then(() => {
|
||||||
|
toastr.success('Guardado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
setUrlHash("aulas,informes");
|
setUrlHash('aulas,informes');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
console.warn('DB.put error', e);
|
console.warn('DB.put error', e);
|
||||||
guardarBtn.disabled = false;
|
guardarBtn.disabled = false;
|
||||||
guardarBtn.style.opacity = "1";
|
guardarBtn.style.opacity = '1';
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
toastr.error("Error al guardar el informe");
|
toastr.error('Error al guardar el informe');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
document.getElementById(btn_borrar).onclick = () => {
|
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(() => {
|
DB.del('aulas_informes', mid).then(() => {
|
||||||
toastr.error("Borrado!");
|
toastr.error('Borrado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setUrlHash("aulas,informes");
|
setUrlHash('aulas,informes');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
edit: function (section) {
|
__decryptIfNeeded: function (table, id, raw) {
|
||||||
if (!checkRole("aulas")) {
|
return new Promise((resolve) => {
|
||||||
setUrlHash("index");
|
if (typeof raw !== 'string') {
|
||||||
|
resolve(raw || {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var item = location.hash.replace("#", "").split(",")[2];
|
TS_decrypt(
|
||||||
|
raw,
|
||||||
|
SECRET,
|
||||||
|
(data) => {
|
||||||
|
resolve(data || {});
|
||||||
|
},
|
||||||
|
table,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
__getServerNow: async function () {
|
||||||
|
try {
|
||||||
|
var couchUrl = (localStorage.getItem('TELESEC_COUCH_URL') || '').replace(/\/$/, '');
|
||||||
|
var couchUser = localStorage.getItem('TELESEC_COUCH_USER') || '';
|
||||||
|
var couchPass = localStorage.getItem('TELESEC_COUCH_PASS') || '';
|
||||||
|
var couchDb = localStorage.getItem('TELESEC_COUCH_DBNAME') || 'telesec';
|
||||||
|
|
||||||
|
if (couchUrl) {
|
||||||
|
var target = couchUrl + '/' + encodeURIComponent(couchDb);
|
||||||
|
var headers = {};
|
||||||
|
if (couchUser) {
|
||||||
|
headers['Authorization'] = 'Basic ' + btoa(couchUser + ':' + couchPass);
|
||||||
|
}
|
||||||
|
var res = await fetch(target, { method: 'HEAD', headers: headers });
|
||||||
|
var dateHeader = res.headers.get('Date');
|
||||||
|
if (dateHeader) {
|
||||||
|
var dt = new Date(dateHeader);
|
||||||
|
if (!isNaN(dt.getTime())) return dt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('No se pudo obtener hora desde CouchDB', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var wres = await fetch('https://worldtimeapi.org/api/timezone/Etc/UTC');
|
||||||
|
if (wres.ok) {
|
||||||
|
var wjson = await wres.json();
|
||||||
|
if (wjson && wjson.utc_datetime) {
|
||||||
|
var wdt = new Date(wjson.utc_datetime);
|
||||||
|
if (!isNaN(wdt.getTime())) return wdt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e2) {
|
||||||
|
console.warn('No se pudo obtener hora desde worldtimeapi', e2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date();
|
||||||
|
},
|
||||||
|
__scheduleShutdown: async function (machineId) {
|
||||||
|
try {
|
||||||
|
document.getElementById('actionStatus').style.display = 'block';
|
||||||
|
var serverNow = await PAGES.aulas.__getServerNow();
|
||||||
|
var shutdownAt = new Date(serverNow.getTime() + 2 * 60 * 1000).toISOString();
|
||||||
|
var raw = await DB.get('aulas_ordenadores', machineId);
|
||||||
|
var data = await PAGES.aulas.__decryptIfNeeded('aulas_ordenadores', machineId, raw);
|
||||||
|
data = data || {};
|
||||||
|
data.Hostname = data.Hostname || machineId;
|
||||||
|
data.ShutdownBeforeDate = shutdownAt;
|
||||||
|
data.ShutdownRequestedAt = serverNow.toISOString();
|
||||||
|
data.ShutdownRequestedBy = SUB_LOGGED_IN_ID || '';
|
||||||
|
await DB.put('aulas_ordenadores', machineId, data);
|
||||||
|
toastr.warning('Apagado programado antes de: ' + shutdownAt);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error programando apagado remoto', e);
|
||||||
|
toastr.error('No se pudo programar el apagado remoto');
|
||||||
|
} finally {
|
||||||
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
__cancelShutdown: async function (machineId) {
|
||||||
|
try {
|
||||||
|
document.getElementById('actionStatus').style.display = 'block';
|
||||||
|
var raw = await DB.get('aulas_ordenadores', machineId);
|
||||||
|
var data = await PAGES.aulas.__decryptIfNeeded('aulas_ordenadores', machineId, raw);
|
||||||
|
data = data || {};
|
||||||
|
data.Hostname = data.Hostname || machineId;
|
||||||
|
data.ShutdownBeforeDate = '';
|
||||||
|
data.ShutdownRequestedAt = '';
|
||||||
|
data.ShutdownRequestedBy = '';
|
||||||
|
await DB.put('aulas_ordenadores', machineId, data);
|
||||||
|
toastr.success('Apagado remoto cancelado');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error cancelando apagado remoto', e);
|
||||||
|
toastr.error('No se pudo cancelar el apagado remoto');
|
||||||
|
} finally {
|
||||||
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ordenadores: function () {
|
||||||
|
container.innerHTML = html`
|
||||||
|
<a class="button" href="#aulas">← Volver a Gestión de Aulas</a>
|
||||||
|
<h1>Control de ordenadores</h1>
|
||||||
|
<p>
|
||||||
|
Estado enviado por el agente Windows. El apagado remoto se programa con hora de servidor.
|
||||||
|
</p>
|
||||||
|
<div id="cont"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
TS_IndexElement(
|
||||||
|
'aulas,ordenadores',
|
||||||
|
[
|
||||||
|
{ key: 'Hostname', type: 'raw', default: '', label: 'Hostname' },
|
||||||
|
{ key: 'UsuarioActual', type: 'raw', default: '', label: 'Usuario actual' },
|
||||||
|
{ key: 'AppActualEjecutable', type: 'raw', default: '', label: 'App actual (exe)' },
|
||||||
|
{ key: 'AppActualTitulo', type: 'raw', default: '', label: 'App actual (título)' },
|
||||||
|
{ key: 'LastSeenAt', type: 'raw', default: '', label: 'Último visto (server)' },
|
||||||
|
{
|
||||||
|
key: 'ShutdownBeforeDate',
|
||||||
|
type: 'template',
|
||||||
|
label: 'Apagado remoto',
|
||||||
|
template: (data, td) => {
|
||||||
|
var text = document.createElement('div');
|
||||||
|
text.style.marginBottom = '6px';
|
||||||
|
text.innerText = data.ShutdownBeforeDate
|
||||||
|
? '⏻ Antes de: ' + data.ShutdownBeforeDate
|
||||||
|
: 'Sin apagado programado';
|
||||||
|
td.appendChild(text);
|
||||||
|
|
||||||
|
var btnOn = document.createElement('button');
|
||||||
|
btnOn.className = 'rojo';
|
||||||
|
btnOn.innerText = 'Programar +2m';
|
||||||
|
btnOn.onclick = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
await PAGES.aulas.__scheduleShutdown(data._key);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
td.appendChild(btnOn);
|
||||||
|
|
||||||
|
if (data.ShutdownBeforeDate) {
|
||||||
|
td.appendChild(document.createElement('br'));
|
||||||
|
var btnCancel = document.createElement('button');
|
||||||
|
btnCancel.className = 'btn5';
|
||||||
|
btnCancel.innerText = 'Cancelar';
|
||||||
|
btnCancel.onclick = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
await PAGES.aulas.__cancelShutdown(data._key);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
td.appendChild(btnCancel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'aulas_ordenadores',
|
||||||
|
document.querySelector('#cont')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
_ordenadores__edit: function (mid) {
|
||||||
|
var field_host = safeuuid();
|
||||||
|
var field_user = safeuuid();
|
||||||
|
var field_exe = safeuuid();
|
||||||
|
var field_title = safeuuid();
|
||||||
|
var field_seen = safeuuid();
|
||||||
|
var field_shutdown = safeuuid();
|
||||||
|
var btn_schedule = safeuuid();
|
||||||
|
var btn_cancel = safeuuid();
|
||||||
|
|
||||||
|
container.innerHTML = html`
|
||||||
|
<a class="button" href="#aulas,ordenadores">← Volver a ordenadores</a>
|
||||||
|
<h1>Ordenador <code>${mid}</code></h1>
|
||||||
|
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
|
||||||
|
<legend>Estado</legend>
|
||||||
|
<label>Hostname<br /><input readonly id="${field_host}" /></label><br /><br />
|
||||||
|
<label>Usuario actual<br /><input readonly id="${field_user}" /></label><br /><br />
|
||||||
|
<label>App actual (exe)<br /><input readonly id="${field_exe}" /></label><br /><br />
|
||||||
|
<label>App actual (título)<br /><input readonly id="${field_title}" /></label><br /><br />
|
||||||
|
<label>Último visto (server)<br /><input readonly id="${field_seen}" /></label><br /><br />
|
||||||
|
<label>ShutdownBeforeDate<br /><input readonly id="${field_shutdown}" /></label><br /><br />
|
||||||
|
<button class="rojo" id="${btn_schedule}">Programar apagado +2m</button>
|
||||||
|
<button class="btn5" id="${btn_cancel}">Cancelar apagado</button>
|
||||||
|
</fieldset>
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
var raw = await DB.get('aulas_ordenadores', mid);
|
||||||
|
var data = await PAGES.aulas.__decryptIfNeeded('aulas_ordenadores', mid, raw);
|
||||||
|
data = data || {};
|
||||||
|
document.getElementById(field_host).value = data.Hostname || mid;
|
||||||
|
document.getElementById(field_user).value = data.UsuarioActual || '';
|
||||||
|
document.getElementById(field_exe).value = data.AppActualEjecutable || '';
|
||||||
|
document.getElementById(field_title).value = data.AppActualTitulo || '';
|
||||||
|
document.getElementById(field_seen).value = data.LastSeenAt || '';
|
||||||
|
document.getElementById(field_shutdown).value = data.ShutdownBeforeDate || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
document.getElementById(btn_schedule).onclick = async () => {
|
||||||
|
await PAGES.aulas.__scheduleShutdown(mid);
|
||||||
|
await loadData();
|
||||||
|
};
|
||||||
|
document.getElementById(btn_cancel).onclick = async () => {
|
||||||
|
await PAGES.aulas.__cancelShutdown(mid);
|
||||||
|
await loadData();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
edit: function (fsection) {
|
||||||
|
if (!checkRole('aulas')) {
|
||||||
|
setUrlHash('index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var section = fsection.split(',')[0];
|
||||||
|
var item = location.hash.replace('#', '').split("?")[0].split(',')[2];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
// No item, show section
|
// No item, show section
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "solicitudes":
|
case 'solicitudes':
|
||||||
this._solicitudes();
|
this._solicitudes();
|
||||||
break;
|
break;
|
||||||
case "informes":
|
case 'informes':
|
||||||
this._informes();
|
this._informes();
|
||||||
break;
|
break;
|
||||||
|
case 'ordenadores':
|
||||||
|
this._ordenadores();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this.index();
|
this.index();
|
||||||
break;
|
break;
|
||||||
@@ -404,12 +685,15 @@ PAGES.aulas = {
|
|||||||
} else {
|
} else {
|
||||||
// Show section__edit
|
// Show section__edit
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "solicitudes":
|
case 'solicitudes':
|
||||||
this._solicitudes__edit(item);
|
this._solicitudes__edit(item);
|
||||||
break;
|
break;
|
||||||
case "informes":
|
case 'informes':
|
||||||
this._informes__edit(item);
|
this._informes__edit(item);
|
||||||
break;
|
break;
|
||||||
|
case 'ordenadores':
|
||||||
|
this._ordenadores__edit(item);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
PERMS["avisos"] = "Avisos"
|
PERMS['avisos'] = 'Avisos';
|
||||||
PERMS["avisos:edit"] = "> Editar"
|
PERMS['avisos:edit'] = '> Editar';
|
||||||
PAGES.avisos = {
|
PAGES.avisos = {
|
||||||
navcss: "btn5",
|
navcss: 'btn5',
|
||||||
icon: "static/appico/File_Plugin.svg",
|
icon: 'static/appico/File_Plugin.svg',
|
||||||
AccessControl: true,
|
AccessControl: true,
|
||||||
Title: "Avisos",
|
Title: 'Avisos',
|
||||||
edit: function (mid) {
|
edit: function (mid) {
|
||||||
if (!checkRole("avisos:edit")) {setUrlHash("avisos");return}
|
if (!checkRole('avisos:edit')) {
|
||||||
|
setUrlHash('avisos');
|
||||||
|
return;
|
||||||
|
}
|
||||||
var nameh1 = safeuuid();
|
var nameh1 = safeuuid();
|
||||||
var field_fecha = safeuuid();
|
var field_fecha = safeuuid();
|
||||||
var field_asunto = safeuuid();
|
var field_asunto = safeuuid();
|
||||||
@@ -20,103 +23,110 @@ PAGES.avisos = {
|
|||||||
var btn_guardar = safeuuid();
|
var btn_guardar = safeuuid();
|
||||||
var btn_borrar = safeuuid();
|
var btn_borrar = safeuuid();
|
||||||
var div_actions = safeuuid();
|
var div_actions = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Aviso <code id="${nameh1}"></code></h1>
|
<h1>Aviso <code id="${nameh1}"></code></h1>
|
||||||
<fieldset style="float: left;">
|
<fieldset style="float: left;">
|
||||||
<legend>Valores</legend>
|
<legend>Valores</legend>
|
||||||
<label>
|
<label>
|
||||||
Fecha<br>
|
Fecha<br />
|
||||||
<input readonly disabled type="text" id="${field_fecha}" value=""><br><br>
|
<input readonly disabled type="text" id="${field_fecha}" value="" /><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Asunto<br>
|
Asunto<br />
|
||||||
<input type="text" id="${field_asunto}" value=""><br><br>
|
<input type="text" id="${field_asunto}" value="" /><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<input type="hidden" id="${field_origen}">
|
<input type="hidden" id="${field_origen}" />
|
||||||
<input type="hidden" id="${field_destino}">
|
<input type="hidden" id="${field_destino}" />
|
||||||
<div id="${div_actions}"></div>
|
<div id="${div_actions}"></div>
|
||||||
<label>
|
<label>
|
||||||
Mensaje<br>
|
Mensaje<br />
|
||||||
<textarea id="${field_mensaje}"></textarea><br><br>
|
<textarea id="${field_mensaje}"></textarea><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Respuesta<br>
|
Respuesta<br />
|
||||||
<textarea id="${field_respuesta}"></textarea><br><br>
|
<textarea id="${field_respuesta}"></textarea><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Estado<br>
|
Estado<br />
|
||||||
<input readonly disabled type="text" id="${field_estado}" value="">
|
<input readonly disabled type="text" id="${field_estado}" value="" />
|
||||||
<br>
|
<br />
|
||||||
<button id="${btn_leer}">Leido</button>
|
<button id="${btn_leer}">Leido</button>
|
||||||
<button id="${btn_desleer}">No leido</button>
|
<button id="${btn_desleer}">No leido</button>
|
||||||
<br>
|
<br />
|
||||||
</label><hr>
|
</label>
|
||||||
|
<hr />
|
||||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
||||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
document.getElementById(btn_leer).onclick = () => {
|
document.getElementById(btn_leer).onclick = () => {
|
||||||
document.getElementById(field_estado).value = "leido";
|
document.getElementById(field_estado).value = 'leido';
|
||||||
};
|
};
|
||||||
document.getElementById(btn_desleer).onclick = () => {
|
document.getElementById(btn_desleer).onclick = () => {
|
||||||
document.getElementById(field_estado).value = "por_leer";
|
document.getElementById(field_estado).value = 'por_leer';
|
||||||
};
|
};
|
||||||
var divact = document.getElementById(div_actions);
|
var divact = document.getElementById(div_actions);
|
||||||
addCategory_Personas(
|
addCategory_Personas(
|
||||||
divact,
|
divact,
|
||||||
SC_Personas,
|
SC_Personas,
|
||||||
"",
|
'',
|
||||||
(value) => {
|
(value) => {
|
||||||
document.getElementById(field_origen).value = value;
|
document.getElementById(field_origen).value = value;
|
||||||
},
|
},
|
||||||
"Origen"
|
'Origen'
|
||||||
);
|
);
|
||||||
addCategory_Personas(
|
addCategory_Personas(
|
||||||
divact,
|
divact,
|
||||||
SC_Personas,
|
SC_Personas,
|
||||||
"",
|
'',
|
||||||
(value) => {
|
(value) => {
|
||||||
document.getElementById(field_destino).value = value;
|
document.getElementById(field_destino).value = value;
|
||||||
},
|
},
|
||||||
"Destino"
|
'Destino'
|
||||||
);
|
);
|
||||||
(async () => {
|
(async () => {
|
||||||
const data = await DB.get('notificaciones', mid);
|
const data = await DB.get('notificaciones', mid);
|
||||||
function load_data(data, ENC = "") {
|
function load_data(data, ENC = '') {
|
||||||
document.getElementById(nameh1).innerText = mid;
|
document.getElementById(nameh1).innerText = mid;
|
||||||
document.getElementById(field_fecha).value = data["Fecha"] || CurrentISODate() || "";
|
document.getElementById(field_fecha).value = data['Fecha'] || CurrentISODate() || '';
|
||||||
document.getElementById(field_asunto).value = data["Asunto"] || "";
|
document.getElementById(field_asunto).value = data['Asunto'] || '';
|
||||||
document.getElementById(field_mensaje).value = data["Mensaje"] || "";
|
document.getElementById(field_mensaje).value = data['Mensaje'] || '';
|
||||||
document.getElementById(field_origen).value = data["Origen"] || SUB_LOGGED_IN_ID || "";
|
document.getElementById(field_origen).value = data['Origen'] || SUB_LOGGED_IN_ID || '';
|
||||||
document.getElementById(field_destino).value = data["Destino"] || "";
|
document.getElementById(field_destino).value = data['Destino'] || '';
|
||||||
document.getElementById(field_estado).value = data["Estado"] || "%%" || "";
|
document.getElementById(field_estado).value = data['Estado'] || '%%' || '';
|
||||||
document.getElementById(field_respuesta).value = data["Respuesta"] || "";
|
document.getElementById(field_respuesta).value = data['Respuesta'] || '';
|
||||||
|
|
||||||
// Persona select
|
// Persona select
|
||||||
divact.innerHTML = "";
|
divact.innerHTML = '';
|
||||||
addCategory_Personas(
|
addCategory_Personas(
|
||||||
divact,
|
divact,
|
||||||
SC_Personas,
|
SC_Personas,
|
||||||
data["Origen"] || "",
|
data['Origen'] || '',
|
||||||
(value) => {
|
(value) => {
|
||||||
document.getElementById(field_origen).value = value;
|
document.getElementById(field_origen).value = value;
|
||||||
},
|
},
|
||||||
"Origen"
|
'Origen'
|
||||||
);
|
);
|
||||||
addCategory_Personas(
|
addCategory_Personas(
|
||||||
divact,
|
divact,
|
||||||
SC_Personas,
|
SC_Personas,
|
||||||
data["Destino"] || "",
|
data['Destino'] || '',
|
||||||
(value) => {
|
(value) => {
|
||||||
document.getElementById(field_destino).value = value;
|
document.getElementById(field_destino).value = value;
|
||||||
},
|
},
|
||||||
"Destino"
|
'Destino'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
TS_decrypt(
|
||||||
load_data(data, "%E");
|
data,
|
||||||
}, 'notificaciones', mid);
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
|
load_data(data, '%E');
|
||||||
|
},
|
||||||
|
'notificaciones',
|
||||||
|
mid
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
load_data(data || {});
|
load_data(data || {});
|
||||||
}
|
}
|
||||||
@@ -127,18 +137,18 @@ PAGES.avisos = {
|
|||||||
if (guardarBtn.disabled) return;
|
if (guardarBtn.disabled) return;
|
||||||
|
|
||||||
// Validate before disabling button
|
// Validate before disabling button
|
||||||
if (document.getElementById(field_origen).value == "") {
|
if (document.getElementById(field_origen).value == '') {
|
||||||
alert("¡Hay que elegir una persona de origen!");
|
alert('¡Hay que elegir una persona de origen!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (document.getElementById(field_destino).value == "") {
|
if (document.getElementById(field_destino).value == '') {
|
||||||
alert("¡Hay que elegir una persona de origen!");
|
alert('¡Hay que elegir una persona de origen!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable button after validation passes
|
// Disable button after validation passes
|
||||||
guardarBtn.disabled = true;
|
guardarBtn.disabled = true;
|
||||||
guardarBtn.style.opacity = "0.5";
|
guardarBtn.style.opacity = '0.5';
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
Fecha: document.getElementById(field_fecha).value,
|
Fecha: document.getElementById(field_fecha).value,
|
||||||
@@ -147,88 +157,91 @@ PAGES.avisos = {
|
|||||||
Mensaje: document.getElementById(field_mensaje).value,
|
Mensaje: document.getElementById(field_mensaje).value,
|
||||||
Respuesta: document.getElementById(field_respuesta).value,
|
Respuesta: document.getElementById(field_respuesta).value,
|
||||||
Asunto: document.getElementById(field_asunto).value,
|
Asunto: document.getElementById(field_asunto).value,
|
||||||
Estado: document
|
Estado: document.getElementById(field_estado).value.replace('%%', 'por_leer'),
|
||||||
.getElementById(field_estado)
|
|
||||||
.value.replace("%%", "por_leer"),
|
|
||||||
};
|
};
|
||||||
document.getElementById("actionStatus").style.display = "block";
|
document.getElementById('actionStatus').style.display = 'block';
|
||||||
DB.put('notificaciones', mid, data).then(() => {
|
DB.put('notificaciones', mid, data)
|
||||||
toastr.success("Guardado!");
|
.then(() => {
|
||||||
|
toastr.success('Guardado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
setUrlHash("avisos");
|
setUrlHash('avisos');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
console.warn('DB.put error', e);
|
console.warn('DB.put error', e);
|
||||||
guardarBtn.disabled = false;
|
guardarBtn.disabled = false;
|
||||||
guardarBtn.style.opacity = "1";
|
guardarBtn.style.opacity = '1';
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
toastr.error("Error al guardar la notificación");
|
toastr.error('Error al guardar la notificación');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
document.getElementById(btn_borrar).onclick = () => {
|
document.getElementById(btn_borrar).onclick = () => {
|
||||||
if (confirm("¿Quieres borrar esta notificación?") == true) {
|
if (confirm('¿Quieres borrar esta notificación?') == true) {
|
||||||
DB.del('notificaciones', mid).then(() => {
|
DB.del('notificaciones', mid).then(() => {
|
||||||
toastr.error("Borrado!");
|
toastr.error('Borrado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setUrlHash("avisos");
|
setUrlHash('avisos');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
index: function () {
|
index: function () {
|
||||||
if (!checkRole("avisos")) {setUrlHash("index");return}
|
if (!checkRole('avisos')) {
|
||||||
|
setUrlHash('index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const tablebody = safeuuid();
|
const tablebody = safeuuid();
|
||||||
var btn_new = safeuuid();
|
var btn_new = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Avisos</h1>
|
<h1>Avisos</h1>
|
||||||
<button id="${btn_new}">Nuevo aviso</button>
|
<button id="${btn_new}">Nuevo aviso</button>
|
||||||
<div id="cont"></div>
|
<div id="cont"></div>
|
||||||
`;
|
`;
|
||||||
TS_IndexElement(
|
TS_IndexElement(
|
||||||
"avisos",
|
'avisos',
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: "Origen",
|
key: 'Origen',
|
||||||
type: "persona",
|
type: 'persona',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Origen",
|
label: 'Origen',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Destino",
|
key: 'Destino',
|
||||||
type: "persona",
|
type: 'persona',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Destino",
|
label: 'Destino',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Asunto",
|
key: 'Asunto',
|
||||||
type: "raw",
|
type: 'raw',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Asunto",
|
label: 'Asunto',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Estado",
|
key: 'Estado',
|
||||||
type: "raw",
|
type: 'raw',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Estado",
|
label: 'Estado',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"notificaciones",
|
'notificaciones',
|
||||||
document.querySelector("#cont"),
|
document.querySelector('#cont'),
|
||||||
(data, new_tr) => {
|
(data, new_tr) => {
|
||||||
new_tr.style.backgroundColor = "#FFCCCB";
|
new_tr.style.backgroundColor = '#FFCCCB';
|
||||||
if (data.Estado == "leido") {
|
if (data.Estado == 'leido') {
|
||||||
new_tr.style.backgroundColor = "lightgreen";
|
new_tr.style.backgroundColor = 'lightgreen';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (!checkRole("avisos:edit")) {
|
if (!checkRole('avisos:edit')) {
|
||||||
document.getElementById(btn_new).style.display = "none"
|
document.getElementById(btn_new).style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
document.getElementById(btn_new).onclick = () => {
|
document.getElementById(btn_new).onclick = () => {
|
||||||
setUrlHash("avisos," + safeuuid(""));
|
setUrlHash('avisos,' + safeuuid(''));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
PAGES.buscar = {
|
PAGES.buscar = {
|
||||||
navcss: "btn1",
|
navcss: 'btn1',
|
||||||
icon: "static/appico/view.svg",
|
icon: 'static/appico/view.svg',
|
||||||
Title: "Buscar",
|
Title: 'Buscar',
|
||||||
AccessControl: true,
|
AccessControl: true,
|
||||||
Esconder: true,
|
Esconder: true,
|
||||||
|
|
||||||
@@ -12,22 +12,23 @@ PAGES.buscar = {
|
|||||||
const recentSearches = safeuuid();
|
const recentSearches = safeuuid();
|
||||||
const moduleFilter = safeuuid();
|
const moduleFilter = safeuuid();
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>🔍 Búsqueda Global</h1>
|
<h1>🔍 Búsqueda Global</h1>
|
||||||
<p>Busca en todos los módulos: personas, materiales, café, comedor, notas y avisos</p>
|
<p>Busca en todos los módulos: personas, materiales, café, comedor, notas y avisos</p>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Opciones de búsqueda</legend>
|
<legend>Opciones de búsqueda</legend>
|
||||||
<input type="text" id="${searchInput}"
|
<input
|
||||||
|
type="text"
|
||||||
|
id="${searchInput}"
|
||||||
placeholder="Escribe aquí para buscar..."
|
placeholder="Escribe aquí para buscar..."
|
||||||
onkeypress="if(event.key==='Enter') document.getElementById('${searchButton}').click()">
|
onkeypress="if(event.key==='Enter') document.getElementById('${searchButton}').click()"
|
||||||
|
/>
|
||||||
<select id="${moduleFilter}">
|
<select id="${moduleFilter}">
|
||||||
<option value="">Todos los módulos</option>
|
<option value="">Todos los módulos</option>
|
||||||
<!-- Options will be populated dynamically based on user permissions -->
|
<!-- Options will be populated dynamically based on user permissions -->
|
||||||
</select>
|
</select>
|
||||||
<button id="${searchButton}" class="btn5">
|
<button id="${searchButton}" class="btn5">Buscar</button>
|
||||||
Buscar
|
|
||||||
</button>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div id="${recentSearches}"></div>
|
<div id="${recentSearches}"></div>
|
||||||
@@ -72,7 +73,7 @@ PAGES.buscar = {
|
|||||||
|
|
||||||
// Add only accessible modules
|
// Add only accessible modules
|
||||||
accessibleModules.forEach((module) => {
|
accessibleModules.forEach((module) => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement('option');
|
||||||
option.value = module.key;
|
option.value = module.key;
|
||||||
option.textContent = `${getModuleIcon(module.key)} ${module.title}`;
|
option.textContent = `${getModuleIcon(module.key)} ${module.title}`;
|
||||||
moduleFilterEl.appendChild(option);
|
moduleFilterEl.appendChild(option);
|
||||||
@@ -82,24 +83,22 @@ PAGES.buscar = {
|
|||||||
// Helper function to get module icons (fallback for older module mappings)
|
// Helper function to get module icons (fallback for older module mappings)
|
||||||
function getModuleIcon(moduleKey) {
|
function getModuleIcon(moduleKey) {
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
personas: "👤",
|
personas: '👤',
|
||||||
materiales: "📦",
|
materiales: '📦',
|
||||||
supercafe: "☕",
|
supercafe: '☕',
|
||||||
comedor: "🍽️",
|
comedor: '🍽️',
|
||||||
avisos: "🔔",
|
avisos: '🔔',
|
||||||
aulas: "🏫",
|
aulas: '🏫',
|
||||||
resumen_diario: "📊",
|
resumen_diario: '📊',
|
||||||
};
|
};
|
||||||
return iconMap[moduleKey] || "📋";
|
return iconMap[moduleKey] || '📋';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load recent searches from localStorage
|
// Load recent searches from localStorage
|
||||||
function loadRecentSearches() {
|
function loadRecentSearches() {
|
||||||
const recent = JSON.parse(
|
const recent = JSON.parse(localStorage.getItem('telesec_recent_searches') || '[]');
|
||||||
localStorage.getItem("telesec_recent_searches") || "[]"
|
|
||||||
);
|
|
||||||
if (recent.length > 0) {
|
if (recent.length > 0) {
|
||||||
recentSearchesEl.innerHTML = `
|
recentSearchesEl.innerHTML = html`
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Búsquedas recientes</legend>
|
<legend>Búsquedas recientes</legend>
|
||||||
${recent
|
${recent
|
||||||
@@ -110,8 +109,11 @@ PAGES.buscar = {
|
|||||||
</button>
|
</button>
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.join("")}
|
.join('')}
|
||||||
<button onclick="localStorage.removeItem('telesec_recent_searches'); this.parentElement.style.display='none';" class="rojo">
|
<button
|
||||||
|
onclick="localStorage.removeItem('telesec_recent_searches'); this.parentElement.style.display='none';"
|
||||||
|
class="rojo"
|
||||||
|
>
|
||||||
Limpiar
|
Limpiar
|
||||||
</button>
|
</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -126,14 +128,12 @@ PAGES.buscar = {
|
|||||||
function saveToRecent(term) {
|
function saveToRecent(term) {
|
||||||
if (!term || term.length < 2) return;
|
if (!term || term.length < 2) return;
|
||||||
|
|
||||||
let recent = JSON.parse(
|
let recent = JSON.parse(localStorage.getItem('telesec_recent_searches') || '[]');
|
||||||
localStorage.getItem("telesec_recent_searches") || "[]"
|
|
||||||
);
|
|
||||||
recent = recent.filter((t) => t !== term); // Remove if exists
|
recent = recent.filter((t) => t !== term); // Remove if exists
|
||||||
recent.unshift(term); // Add to beginning
|
recent.unshift(term); // Add to beginning
|
||||||
recent = recent.slice(0, 5); // Keep only 5 most recent
|
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();
|
loadRecentSearches();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ PAGES.buscar = {
|
|||||||
const selectedModule = moduleFilterEl.value;
|
const selectedModule = moduleFilterEl.value;
|
||||||
|
|
||||||
if (searchTerm.length < 2) {
|
if (searchTerm.length < 2) {
|
||||||
resultsEl.innerHTML = `
|
resultsEl.innerHTML = html`
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Error</legend>
|
<legend>Error</legend>
|
||||||
<div>⚠️ Por favor, introduce al menos 2 caracteres para buscar</div>
|
<div>⚠️ Por favor, introduce al menos 2 caracteres para buscar</div>
|
||||||
@@ -153,7 +153,7 @@ PAGES.buscar = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show loading
|
// Show loading
|
||||||
resultsEl.innerHTML = `
|
resultsEl.innerHTML = html`
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Buscando...</legend>
|
<legend>Buscando...</legend>
|
||||||
<div>⏳ Procesando búsqueda...</div>
|
<div>⏳ Procesando búsqueda...</div>
|
||||||
@@ -166,9 +166,7 @@ PAGES.buscar = {
|
|||||||
|
|
||||||
// Filter by module if selected
|
// Filter by module if selected
|
||||||
if (selectedModule) {
|
if (selectedModule) {
|
||||||
results = results.filter(
|
results = results.filter((result) => result._module === selectedModule);
|
||||||
(result) => result._module === selectedModule
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
globalSearch.renderResults(results, resultsEl);
|
globalSearch.renderResults(results, resultsEl);
|
||||||
@@ -176,16 +174,17 @@ PAGES.buscar = {
|
|||||||
|
|
||||||
// Add stats
|
// Add stats
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
const statsDiv = document.createElement("fieldset");
|
const statsDiv = document.createElement('fieldset');
|
||||||
const legend = document.createElement("legend");
|
const legend = document.createElement('legend');
|
||||||
legend.textContent = "Estadísticas";
|
legend.textContent = 'Estadísticas';
|
||||||
statsDiv.appendChild(legend);
|
statsDiv.appendChild(legend);
|
||||||
|
|
||||||
let filterText = selectedModule
|
let filterText = selectedModule
|
||||||
? ` en ${moduleFilterEl.options[moduleFilterEl.selectedIndex].text}`
|
? ` en ${moduleFilterEl.options[moduleFilterEl.selectedIndex].text}`
|
||||||
: "";
|
: '';
|
||||||
const content = document.createElement("div");
|
const content = document.createElement('div');
|
||||||
content.innerHTML = `📊 Se encontraron <strong>${results.length}</strong> resultados para "<strong>${searchTerm}</strong>"${filterText}`;
|
content.innerHTML = html`📊 Se encontraron <strong>${results.length}</strong> resultados
|
||||||
|
para "<strong>${searchTerm}</strong>"${filterText}`;
|
||||||
statsDiv.appendChild(content);
|
statsDiv.appendChild(content);
|
||||||
|
|
||||||
resultsEl.insertBefore(statsDiv, resultsEl.firstChild);
|
resultsEl.insertBefore(statsDiv, resultsEl.firstChild);
|
||||||
@@ -218,19 +217,19 @@ PAGES.buscar = {
|
|||||||
searchInputEl.focus();
|
searchInputEl.focus();
|
||||||
|
|
||||||
// Add keyboard shortcuts
|
// Add keyboard shortcuts
|
||||||
document.addEventListener("keydown", function (e) {
|
document.addEventListener('keydown', function (e) {
|
||||||
// Ctrl+F or Cmd+F to focus search
|
// 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();
|
e.preventDefault();
|
||||||
searchInputEl.focus();
|
searchInputEl.focus();
|
||||||
searchInputEl.select();
|
searchInputEl.select();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escape to clear search
|
// Escape to clear search
|
||||||
if (e.key === "Escape") {
|
if (e.key === 'Escape') {
|
||||||
searchInputEl.value = "";
|
searchInputEl.value = '';
|
||||||
searchInputEl.focus();
|
searchInputEl.focus();
|
||||||
resultsEl.innerHTML = `
|
resultsEl.innerHTML = html`
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Resultados</legend>
|
<legend>Resultados</legend>
|
||||||
<div>🔍 Introduce un término de búsqueda para comenzar</div>
|
<div>🔍 Introduce un término de búsqueda para comenzar</div>
|
||||||
@@ -252,10 +251,10 @@ PAGES.buscar = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check for quick search term from header
|
// Check for quick search term from header
|
||||||
const quickSearchTerm = sessionStorage.getItem("telesec_quick_search");
|
const quickSearchTerm = sessionStorage.getItem('telesec_quick_search');
|
||||||
if (quickSearchTerm) {
|
if (quickSearchTerm) {
|
||||||
searchInputEl.value = quickSearchTerm;
|
searchInputEl.value = quickSearchTerm;
|
||||||
sessionStorage.removeItem("telesec_quick_search");
|
sessionStorage.removeItem('telesec_quick_search');
|
||||||
// Perform search automatically
|
// Perform search automatically
|
||||||
setTimeout(performSearch, 100);
|
setTimeout(performSearch, 100);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,170 @@
|
|||||||
PERMS["comedor"] = "Comedor"
|
PERMS['comedor'] = 'Comedor';
|
||||||
PERMS["comedor:edit"] = "> Editar"
|
PERMS['comedor:edit'] = '> Editar';
|
||||||
PAGES.comedor = {
|
PAGES.comedor = {
|
||||||
navcss: "btn6",
|
navcss: 'btn6',
|
||||||
icon: "static/appico/apple.png",
|
icon: 'static/appico/apple.png',
|
||||||
AccessControl: true,
|
AccessControl: true,
|
||||||
Title: "Comedor",
|
Title: 'Comedor',
|
||||||
|
__cleanupOldMenus: async function () {
|
||||||
|
try {
|
||||||
|
var rows = await DB.list('comedor');
|
||||||
|
var now = new Date();
|
||||||
|
var todayUTC = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
||||||
|
var removed = 0;
|
||||||
|
|
||||||
|
function parseISODateToUTC(value) {
|
||||||
|
if (!value || typeof value !== 'string') return null;
|
||||||
|
var match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) return null;
|
||||||
|
var y = parseInt(match[1], 10);
|
||||||
|
var m = parseInt(match[2], 10) - 1;
|
||||||
|
var d = parseInt(match[3], 10);
|
||||||
|
return Date.UTC(y, m, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFechaFromRow(row) {
|
||||||
|
var data = row.data;
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
TS_decrypt(
|
||||||
|
data,
|
||||||
|
SECRET,
|
||||||
|
(decrypted) => {
|
||||||
|
if (decrypted && typeof decrypted === 'object') {
|
||||||
|
resolve(decrypted.Fecha || row.id.split(',')[0] || '');
|
||||||
|
} else {
|
||||||
|
resolve(row.id.split(',')[0] || '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'comedor',
|
||||||
|
row.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
return data.Fecha || row.id.split(',')[0] || '';
|
||||||
|
}
|
||||||
|
return row.id.split(',')[0] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
var fecha = await getFechaFromRow(row);
|
||||||
|
var rowUTC = parseISODateToUTC(fecha);
|
||||||
|
if (rowUTC == null) continue;
|
||||||
|
var ageDays = Math.floor((todayUTC - rowUTC) / 86400000);
|
||||||
|
if (ageDays >= 30) {
|
||||||
|
await DB.del('comedor', row.id);
|
||||||
|
removed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removed > 0) {
|
||||||
|
toastr.info('Limpieza automática: ' + removed + ' menús antiguos eliminados.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Comedor cleanup error', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
edit: function (mid) {
|
edit: function (mid) {
|
||||||
if (!checkRole("comedor:edit")) {setUrlHash("comedor");return}
|
if (!checkRole('comedor:edit')) {
|
||||||
|
setUrlHash('comedor');
|
||||||
|
return;
|
||||||
|
}
|
||||||
var nameh1 = safeuuid();
|
var nameh1 = safeuuid();
|
||||||
var field_fecha = safeuuid();
|
var field_fecha = safeuuid();
|
||||||
var field_platos = safeuuid();
|
var field_tipo = safeuuid();
|
||||||
|
var field_primero = safeuuid();
|
||||||
|
var field_segundo = safeuuid();
|
||||||
|
var field_postre = safeuuid();
|
||||||
|
var btn_picto_primero = safeuuid();
|
||||||
|
var btn_picto_segundo = safeuuid();
|
||||||
|
var btn_picto_postre = safeuuid();
|
||||||
|
var debounce_picto = safeuuid();
|
||||||
var btn_guardar = safeuuid();
|
var btn_guardar = safeuuid();
|
||||||
var btn_borrar = safeuuid();
|
var btn_borrar = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Entrada del menú <code id="${nameh1}"></code></h1>
|
<h1>Entrada del menú <code id="${nameh1}"></code></h1>
|
||||||
<fieldset style="float: left;">
|
<fieldset style="float: left;">
|
||||||
<legend>Valores</legend>
|
<legend>Valores</legend>
|
||||||
<label>
|
<label>
|
||||||
Fecha<br>
|
Fecha<br />
|
||||||
<input type="date" id="${field_fecha}" value=""><br><br>
|
<input type="date" id="${field_fecha}" value="" /><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Platos<br>
|
Tipo<br />
|
||||||
<textarea id="${field_platos}"></textarea><br><br>
|
<input type="text" id="${field_tipo}" value="" /><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
<label>
|
||||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
Primero<br />
|
||||||
|
<input type="text" id="${field_primero}" value="" /><br />
|
||||||
|
<div class="picto" id="${btn_picto_primero}"></div>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Segundo<br />
|
||||||
|
<input type="text" id="${field_segundo}" value="" /><br />
|
||||||
|
<div class="picto" id="${btn_picto_segundo}"></div>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Postre<br />
|
||||||
|
<input type="text" id="${field_postre}" value="" /><br />
|
||||||
|
<div class="picto" id="${btn_picto_postre}"></div>
|
||||||
|
</label>
|
||||||
|
<button class="saveico" id="${btn_guardar}">
|
||||||
|
<img src="static/floppy_disk_green.png" />
|
||||||
|
<br>Guardar
|
||||||
|
</button>
|
||||||
|
<button class="delico" id="${btn_borrar}">
|
||||||
|
<img src="static/garbage.png" />
|
||||||
|
<br>Borrar
|
||||||
|
</button>
|
||||||
|
<button class="opicon" onclick="setUrlHash('comedor')" style="float: right;"> <!-- Align to the right -->
|
||||||
|
<img src="static/exit.png" />
|
||||||
|
<br>Salir
|
||||||
|
</button>
|
||||||
|
<button class="opicon" onclick="window.print()" style="float: right;"> <!-- Align to the right -->
|
||||||
|
<img src="static/printer2.png" />
|
||||||
|
<br>Imprimir
|
||||||
|
</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
|
const pictogramSelector = TS_CreateArasaacSelector({
|
||||||
|
modal: true,
|
||||||
|
debounceId: debounce_picto,
|
||||||
|
onPick: (context, item) => {
|
||||||
|
TS_applyPictoValue(context.pictoId, {
|
||||||
|
text: item.label,
|
||||||
|
arasaacId: String(item.id),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
document.getElementById(btn_picto_primero).onclick = () =>
|
||||||
|
pictogramSelector.open({ pictoId: btn_picto_primero });
|
||||||
|
document.getElementById(btn_picto_segundo).onclick = () =>
|
||||||
|
pictogramSelector.open({ pictoId: btn_picto_segundo });
|
||||||
|
document.getElementById(btn_picto_postre).onclick = () =>
|
||||||
|
pictogramSelector.open({ pictoId: btn_picto_postre });
|
||||||
DB.get('comedor', mid).then((data) => {
|
DB.get('comedor', mid).then((data) => {
|
||||||
function load_data(data, ENC = "") {
|
function load_data(data, ENC = '') {
|
||||||
document.getElementById(nameh1).innerText = mid;
|
document.getElementById(nameh1).innerText = mid;
|
||||||
document.getElementById(field_fecha).value = data["Fecha"] || mid || CurrentISODate();
|
document.getElementById(field_fecha).value = data['Fecha'] || mid || CurrentISODate();
|
||||||
document.getElementById(field_platos).value =
|
document.getElementById(field_tipo).value = data['Tipo'] || '';
|
||||||
data["Platos"] || "";
|
document.getElementById(field_primero).value = data['Primero'] || '';
|
||||||
|
document.getElementById(field_segundo).value = data['Segundo'] || '';
|
||||||
|
document.getElementById(field_postre).value = data['Postre'] || '';
|
||||||
|
TS_applyPictoValue(btn_picto_primero, data['Primero_Picto'] || '');
|
||||||
|
TS_applyPictoValue(btn_picto_segundo, data['Segundo_Picto'] || '');
|
||||||
|
TS_applyPictoValue(btn_picto_postre, data['Postre_Picto'] || '');
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
TS_decrypt(
|
||||||
load_data(data, "%E");
|
data,
|
||||||
}, 'comedor', mid);
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
|
load_data(data, '%E');
|
||||||
|
},
|
||||||
|
'comedor',
|
||||||
|
mid
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
load_data(data || {});
|
load_data(data || {});
|
||||||
}
|
}
|
||||||
@@ -49,85 +175,122 @@ PAGES.comedor = {
|
|||||||
if (guardarBtn.disabled) return;
|
if (guardarBtn.disabled) return;
|
||||||
|
|
||||||
guardarBtn.disabled = true;
|
guardarBtn.disabled = true;
|
||||||
guardarBtn.style.opacity = "0.5";
|
guardarBtn.style.opacity = '0.5';
|
||||||
|
|
||||||
const newDate = document.getElementById(field_fecha).value;
|
const newDate = document.getElementById(field_fecha).value;
|
||||||
|
const newTipo = document.getElementById(field_tipo).value.trim();
|
||||||
var data = {
|
var data = {
|
||||||
Fecha: newDate,
|
Fecha: newDate,
|
||||||
Platos: document.getElementById(field_platos).value,
|
Tipo: newTipo,
|
||||||
|
Primero: document.getElementById(field_primero).value.trim(),
|
||||||
|
Segundo: document.getElementById(field_segundo).value.trim(),
|
||||||
|
Postre: document.getElementById(field_postre).value.trim(),
|
||||||
|
Primero_Picto: TS_getPictoValue(btn_picto_primero),
|
||||||
|
Segundo_Picto: TS_getPictoValue(btn_picto_segundo),
|
||||||
|
Postre_Picto: TS_getPictoValue(btn_picto_postre),
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the date has changed, we need to delete the old entry
|
// If the date has changed, we need to delete the old entry
|
||||||
if (mid !== newDate && mid !== "") {
|
if (mid !== newDate + "," + newTipo && mid !== '') {
|
||||||
DB.del('comedor', mid);
|
DB.del('comedor', mid);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("actionStatus").style.display = "block";
|
document.getElementById('actionStatus').style.display = 'block';
|
||||||
DB.put('comedor', newDate, data).then(() => {
|
DB.put('comedor', newDate + "," + newTipo, data)
|
||||||
toastr.success("Guardado!");
|
.then(() => {
|
||||||
|
toastr.success('Guardado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
setUrlHash("comedor");
|
setUrlHash('comedor');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
console.warn('DB.put error', e);
|
console.warn('DB.put error', e);
|
||||||
guardarBtn.disabled = false;
|
guardarBtn.disabled = false;
|
||||||
guardarBtn.style.opacity = "1";
|
guardarBtn.style.opacity = '1';
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
toastr.error("Error al guardar el menú");
|
toastr.error('Error al guardar el menú');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
document.getElementById(btn_borrar).onclick = () => {
|
document.getElementById(btn_borrar).onclick = () => {
|
||||||
if (confirm("¿Quieres borrar esta entrada?") == true) {
|
if (confirm('¿Quieres borrar esta entrada?') == true) {
|
||||||
DB.del('comedor', mid).then(() => {
|
DB.del('comedor', mid).then(() => {
|
||||||
toastr.error("Borrado!");
|
toastr.error('Borrado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setUrlHash("comedor");
|
setUrlHash('comedor');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
index: function () {
|
index: function () {
|
||||||
if (!checkRole("comedor")) {setUrlHash("index");return}
|
if (!checkRole('comedor')) {
|
||||||
|
setUrlHash('index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const cont = safeuuid();
|
const cont = safeuuid();
|
||||||
var btn_new = safeuuid();
|
var btn_new = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Menú del comedor</h1>
|
<h1>Menú del comedor</h1>
|
||||||
<button id="${btn_new}">Nueva entrada</button>
|
<button id="${btn_new}">Nueva entrada</button>
|
||||||
<div id="${cont}"></div>
|
<div id="${cont}"></div>
|
||||||
`;
|
`;
|
||||||
|
var renderList = () => {
|
||||||
TS_IndexElement(
|
TS_IndexElement(
|
||||||
"comedor",
|
'comedor',
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: "Fecha",
|
key: 'Fecha',
|
||||||
type: "raw",
|
type: 'raw',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Fecha",
|
label: 'Fecha',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Platos",
|
key: 'Tipo',
|
||||||
type: "raw",
|
type: 'raw',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Platos",
|
label: 'Tipo',
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
key: 'Primero_Picto',
|
||||||
|
type: 'picto',
|
||||||
|
default: '',
|
||||||
|
label: 'Primero',
|
||||||
|
labelkey: 'Primero',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Segundo_Picto',
|
||||||
|
type: 'picto',
|
||||||
|
default: '',
|
||||||
|
label: 'Segundo',
|
||||||
|
labelkey: 'Segundo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Postre_Picto',
|
||||||
|
type: 'picto',
|
||||||
|
default: '',
|
||||||
|
label: 'Postre',
|
||||||
|
labelkey: 'Postre',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"comedor",
|
'comedor',
|
||||||
document.getElementById(cont),
|
document.getElementById(cont),
|
||||||
(data, new_tr) => {
|
(data, new_tr) => {
|
||||||
// new_tr.style.backgroundColor = "#FFCCCB";
|
// new_tr.style.backgroundColor = "#FFCCCB";
|
||||||
if (data.Fecha == CurrentISODate()) {
|
if (data.Fecha == CurrentISODate()) {
|
||||||
new_tr.style.backgroundColor = "lightgreen";
|
new_tr.style.backgroundColor = 'lightgreen';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if (!checkRole("comedor:edit")) {
|
PAGES.comedor.__cleanupOldMenus().finally(renderList);
|
||||||
document.getElementById(btn_new).style.display = "none"
|
|
||||||
|
if (!checkRole('comedor:edit')) {
|
||||||
|
document.getElementById(btn_new).style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
document.getElementById(btn_new).onclick = () => {
|
document.getElementById(btn_new).onclick = () => {
|
||||||
setUrlHash("comedor," + safeuuid(""));
|
setUrlHash('comedor,' + safeuuid(''));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,34 +1,37 @@
|
|||||||
PAGES.dataman = {
|
PAGES.dataman = {
|
||||||
navcss: "btn1",
|
navcss: 'btn1',
|
||||||
icon: "static/appico/gear_edit.png",
|
icon: 'static/appico/gear_edit.png',
|
||||||
AccessControl: true,
|
AccessControl: true,
|
||||||
Title: "Ajustes",
|
Title: 'Ajustes',
|
||||||
edit: function (mid) {
|
edit: function (mid) {
|
||||||
switch (mid) {
|
switch (mid) {
|
||||||
case "export":
|
case 'export':
|
||||||
PAGES.dataman.__export();
|
PAGES.dataman.__export();
|
||||||
break;
|
break;
|
||||||
case "import":
|
case 'import':
|
||||||
PAGES.dataman.__import();
|
PAGES.dataman.__import();
|
||||||
break;
|
break;
|
||||||
case "config":
|
case 'config':
|
||||||
PAGES.dataman.__config();
|
PAGES.dataman.__config();
|
||||||
break;
|
break;
|
||||||
case "labels":
|
case 'labels':
|
||||||
PAGES.dataman.__labels();
|
PAGES.dataman.__labels();
|
||||||
break;
|
break;
|
||||||
|
case 'precios':
|
||||||
|
PAGES.dataman.__precios();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Tab to edit
|
// Tab to edit
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
__config: function () {
|
__config: function () {
|
||||||
var form = safeuuid();
|
var form = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Ajustes</h1>
|
<h1>Ajustes</h1>
|
||||||
<h2>No disponible</h2>
|
<h2>No disponible</h2>
|
||||||
<form id="${form}">
|
<form id="${form}">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="block_add_account" value="yes">
|
<input type="checkbox" name="block_add_account" value="yes" />
|
||||||
<b>Bloquear crear cuenta de administrador?</b>
|
<b>Bloquear crear cuenta de administrador?</b>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit">Aplicar ajustes</button>
|
<button type="submit">Aplicar ajustes</button>
|
||||||
@@ -37,29 +40,23 @@ PAGES.dataman = {
|
|||||||
document.getElementById(form).onsubmit = (ev) => {
|
document.getElementById(form).onsubmit = (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
var ford = new FormData(document.getElementById(form));
|
var ford = new FormData(document.getElementById(form));
|
||||||
if (ford.get("block_add_account") == "yes") {
|
if (ford.get('block_add_account') == 'yes') {
|
||||||
config["block_add_account"] = true;
|
config['block_add_account'] = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
__export: function () {
|
__export: function () {
|
||||||
var select_type = safeuuid();
|
|
||||||
var textarea_content = safeuuid();
|
|
||||||
var button_export_local = safeuuid();
|
var button_export_local = safeuuid();
|
||||||
var button_export_safe = safeuuid();
|
var button_export_safe = safeuuid();
|
||||||
var button_export_safe_cloud = safeuuid();
|
container.innerHTML = html`
|
||||||
var button_clear = safeuuid();
|
|
||||||
container.innerHTML = `
|
|
||||||
<h1>Exportar Datos</h1>
|
<h1>Exportar Datos</h1>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Exportar datos</legend>
|
<legend>Exportar datos</legend>
|
||||||
<em>Al pulsar, Espera hasta que salga una notificacion verde.</em>
|
<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_local}" type="button">Exportar sin cifrar</button>
|
||||||
<button id="${button_export_safe}" type="button">Exportar con cifrado</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>
|
|
||||||
<!--<br><br><em>Para descargar envia un correo a telesec@tech.eus con el asunto "TSBK %${getDBName()}".</em>-->
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
document.getElementById(button_export_local).onclick = () => {
|
document.getElementById(button_export_local).onclick = () => {
|
||||||
@@ -70,80 +67,71 @@ PAGES.dataman = {
|
|||||||
};
|
};
|
||||||
(async () => {
|
(async () => {
|
||||||
const materiales = await DB.list('materiales');
|
const materiales = await DB.list('materiales');
|
||||||
materiales.forEach(entry => {
|
materiales.forEach((entry) => {
|
||||||
const key = entry.id;
|
const key = entry.id;
|
||||||
const value = entry.data;
|
const value = entry.data;
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
if (typeof value == 'string') {
|
if (typeof value == 'string') {
|
||||||
TS_decrypt(value, SECRET, (data, wasEncrypted) => {
|
TS_decrypt(
|
||||||
|
value,
|
||||||
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
output.materiales[key] = data;
|
output.materiales[key] = data;
|
||||||
}, 'materiales', key);
|
},
|
||||||
|
'materiales',
|
||||||
|
key
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
output.materiales[key] = value;
|
output.materiales[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const personas = await DB.list('personas');
|
const personas = await DB.list('personas');
|
||||||
personas.forEach(entry => {
|
personas.forEach((entry) => {
|
||||||
const key = entry.id;
|
const key = entry.id;
|
||||||
const value = entry.data;
|
const value = entry.data;
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
if (typeof value == 'string') {
|
if (typeof value == 'string') {
|
||||||
TS_decrypt(value, SECRET, (data, wasEncrypted) => {
|
TS_decrypt(
|
||||||
|
value,
|
||||||
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
output.personas[key] = data;
|
output.personas[key] = data;
|
||||||
}, 'personas', key);
|
},
|
||||||
|
'personas',
|
||||||
|
key
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
output.personas[key] = value;
|
output.personas[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
toastr.success("Exportado todo, descargando!");
|
toastr.success('Exportado todo, descargando!');
|
||||||
download(
|
download(`Export %%TITLE%% ${getDBName()}.json.txt`, JSON.stringify(output));
|
||||||
`Export %%TITLE%% ${getDBName()}.json.txt`,
|
|
||||||
JSON.stringify(output)
|
|
||||||
);
|
|
||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
document.getElementById(button_export_safe).onclick = () => {
|
document.getElementById(button_export_safe).onclick = () => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const result = { materiales: {}, personas: {} };
|
const result = { materiales: {}, personas: {} };
|
||||||
const materiales = await DB.list('materiales');
|
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');
|
const personas = await DB.list('personas');
|
||||||
personas.forEach(entry => { result.personas[entry.id] = entry.data; });
|
personas.forEach((entry) => {
|
||||||
toastr.success("Exportado todo, descargado!");
|
result.personas[entry.id] = entry.data;
|
||||||
download(
|
});
|
||||||
`Export %%TITLE%% Encriptado ${getDBName()}.json.txt`,
|
toastr.success('Exportado todo, descargado!');
|
||||||
JSON.stringify(result)
|
download(`Export %%TITLE%% Encriptado ${getDBName()}.json.txt`, JSON.stringify(result));
|
||||||
);
|
|
||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
// document.getElementById(button_export_safe_cloud).onclick = () => {
|
|
||||||
// var download_data = (DATA) => {
|
|
||||||
// toastr.info("Exportado todo, subiendo!");
|
|
||||||
// fetch(
|
|
||||||
// "https://telesec-sync.tech.eus/upload_backup.php?table=" + getDBName(),
|
|
||||||
// {
|
|
||||||
// method: "POST",
|
|
||||||
// body: JSON.stringify(DATA),
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// .then(() => {
|
|
||||||
// toastr.success("Subido correctamente!");
|
|
||||||
// })
|
|
||||||
// .catch(() => {
|
|
||||||
// toastr.error("Ha ocurrido un error en la subida.");
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
// gun.get(TABLE).load(download_data);
|
|
||||||
// };
|
|
||||||
},
|
},
|
||||||
__import: function () {
|
__import: function () {
|
||||||
var select_type = safeuuid();
|
var select_type = safeuuid();
|
||||||
var textarea_content = safeuuid();
|
var textarea_content = safeuuid();
|
||||||
var button_import = safeuuid();
|
var button_import = safeuuid();
|
||||||
var button_clear = safeuuid();
|
var button_clear = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Importar Datos</h1>
|
<h1>Importar Datos</h1>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Importar datos</legend>
|
<legend>Importar datos</legend>
|
||||||
@@ -157,16 +145,20 @@ PAGES.dataman = {
|
|||||||
<option value="comandas">Galileo - db.cafe.comandas.axd</option>
|
<option value="comandas">Galileo - db.cafe.comandas.axd</option>
|
||||||
<option value="%telesec">TeleSec Exportado (encriptado o no)</option>
|
<option value="%telesec">TeleSec Exportado (encriptado o no)</option>
|
||||||
</select>
|
</select>
|
||||||
<textarea id="${textarea_content}" style="height: 100px;" placeholder="Introduce el contenido del archivo"></textarea>
|
<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_import}" type="button">Importar</button>
|
||||||
<button id="${button_clear}" type="button">Vaciar</button>
|
<button id="${button_clear}" type="button">Vaciar</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
document.getElementById(button_import).onclick = () => {
|
document.getElementById(button_import).onclick = () => {
|
||||||
toastr.info("Importando datos...");
|
toastr.info('Importando datos...');
|
||||||
var val = document.getElementById(textarea_content).value;
|
var val = document.getElementById(textarea_content).value;
|
||||||
var sel = document.getElementById(select_type).value;
|
var sel = document.getElementById(select_type).value;
|
||||||
if (sel == "%telesec") {
|
if (sel == '%telesec') {
|
||||||
// legacy import, store entire payload as-is
|
// legacy import, store entire payload as-is
|
||||||
// for each top-level key, store their items in DB
|
// for each top-level key, store their items in DB
|
||||||
var parsed = JSON.parse(val);
|
var parsed = JSON.parse(val);
|
||||||
@@ -174,19 +166,23 @@ PAGES.dataman = {
|
|||||||
const sectionName = section[0];
|
const sectionName = section[0];
|
||||||
const sectionData = section[1];
|
const sectionData = section[1];
|
||||||
Object.entries(sectionData).forEach((entry) => {
|
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 {
|
} else {
|
||||||
Object.entries(JSON.parse(val)["data"]).forEach((entry) => {
|
Object.entries(JSON.parse(val)['data']).forEach((entry) => {
|
||||||
DB.put(sel, entry[0], entry[1]).catch((e) => { console.warn('DB.put error', e); });
|
DB.put(sel, entry[0], entry[1]).catch((e) => {
|
||||||
|
console.warn('DB.put error', e);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
toastr.info("Importado todo!");
|
toastr.info('Importado todo!');
|
||||||
|
|
||||||
if (sel == "%telesec") {
|
if (sel == '%telesec') {
|
||||||
setUrlHash("inicio");
|
setUrlHash('inicio');
|
||||||
} else {
|
} else {
|
||||||
setUrlHash(sel);
|
setUrlHash(sel);
|
||||||
}
|
}
|
||||||
@@ -195,23 +191,19 @@ PAGES.dataman = {
|
|||||||
},
|
},
|
||||||
__labels: function (mid) {
|
__labels: function (mid) {
|
||||||
var div_materiales = safeuuid();
|
var div_materiales = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html` <h1>Imprimir Etiquetas QR</h1>
|
||||||
<h1>Imprimir Etiquetas QR</h1>
|
|
||||||
<button onclick="print()">Imprimir</button>
|
<button onclick="print()">Imprimir</button>
|
||||||
<h2>Materiales</h2>
|
<h2>Materiales</h2>
|
||||||
<div id="${div_materiales}"></div>
|
<div id="${div_materiales}"></div>
|
||||||
<br><br>`;
|
<br /><br />`;
|
||||||
div_materiales = document.getElementById(div_materiales);
|
div_materiales = document.getElementById(div_materiales);
|
||||||
DB.map('materiales', (data, key) => {
|
DB.map('materiales', (data, key) => {
|
||||||
function add_row(data, key) {
|
function add_row(data, key) {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
div_materiales.innerHTML += BuildQR(
|
div_materiales.innerHTML += BuildQR('materiales,' + key, data['Nombre'] || key);
|
||||||
"materiales," + key,
|
|
||||||
data["Nombre"] || key
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data) => {
|
TS_decrypt(data, SECRET, (data) => {
|
||||||
add_row(data, key);
|
add_row(data, key);
|
||||||
});
|
});
|
||||||
@@ -220,12 +212,88 @@ PAGES.dataman = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
__precios: function () {
|
||||||
|
var form = safeuuid();
|
||||||
|
|
||||||
|
// Cargar precios actuales desde DB
|
||||||
|
DB.get('config', 'precios_cafe').then((raw) => {
|
||||||
|
TS_decrypt(raw, SECRET, (precios) => {
|
||||||
|
container.innerHTML = html`
|
||||||
|
<h1>Configuración de Precios del Café</h1>
|
||||||
|
<form id="${form}">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Precios Base (en céntimos)</legend>
|
||||||
|
<label>
|
||||||
|
<b>Servicio base:</b>
|
||||||
|
<input type="number" name="servicio_base" value="${precios.servicio_base || 10}" min="0" step="1" />
|
||||||
|
céntimos
|
||||||
|
</label>
|
||||||
|
<br><br>
|
||||||
|
<label>
|
||||||
|
<b>Leche pequeña:</b>
|
||||||
|
<input type="number" name="leche_pequena" value="${precios.leche_pequena || 15}" min="0" step="1" />
|
||||||
|
céntimos
|
||||||
|
</label>
|
||||||
|
<br><br>
|
||||||
|
<label>
|
||||||
|
<b>Leche grande:</b>
|
||||||
|
<input type="number" name="leche_grande" value="${precios.leche_grande || 25}" min="0" step="1" />
|
||||||
|
céntimos
|
||||||
|
</label>
|
||||||
|
<br><br>
|
||||||
|
<label>
|
||||||
|
<b>Café:</b>
|
||||||
|
<input type="number" name="cafe" value="${precios.cafe || 25}" min="0" step="1" />
|
||||||
|
céntimos
|
||||||
|
</label>
|
||||||
|
<br><br>
|
||||||
|
<label>
|
||||||
|
<b>ColaCao:</b>
|
||||||
|
<input type="number" name="colacao" value="${precios.colacao || 25}" min="0" step="1" />
|
||||||
|
céntimos
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<br>
|
||||||
|
<button type="submit">💾 Guardar precios</button>
|
||||||
|
<button type="button" onclick="setUrlHash('dataman')">🔙 Volver</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById(form).onsubmit = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
var formData = new FormData(document.getElementById(form));
|
||||||
|
var nuevosPrecios = {
|
||||||
|
servicio_base: parseInt(formData.get('servicio_base')) || 10,
|
||||||
|
leche_pequena: parseInt(formData.get('leche_pequena')) || 15,
|
||||||
|
leche_grande: parseInt(formData.get('leche_grande')) || 25,
|
||||||
|
cafe: parseInt(formData.get('cafe')) || 25,
|
||||||
|
colacao: parseInt(formData.get('colacao')) || 25,
|
||||||
|
};
|
||||||
|
|
||||||
|
DB.put('config', 'precios_cafe', nuevosPrecios).then(() => {
|
||||||
|
toastr.success('Precios guardados correctamente');
|
||||||
|
// Actualizar variable global
|
||||||
|
if (window.PRECIOS_CAFE) {
|
||||||
|
Object.assign(window.PRECIOS_CAFE, nuevosPrecios);
|
||||||
|
}
|
||||||
|
setTimeout(() => setUrlHash('dataman'), 1000);
|
||||||
|
}).catch((e) => {
|
||||||
|
toastr.error('Error al guardar precios: ' + e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
// Si no hay precios guardados, usar valores por defecto
|
||||||
|
PAGES.dataman.__precios();
|
||||||
|
});
|
||||||
|
},
|
||||||
index: function () {
|
index: function () {
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Administración de datos</h1>
|
<h1>Administración de datos</h1>
|
||||||
<a class="button" href="#dataman,import">Importar datos</a>
|
<a class="button" href="#dataman,import">Importar datos</a>
|
||||||
<a class="button" href="#dataman,export">Exportar 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,labels">Imprimir etiquetas</a>
|
||||||
|
<a class="button" href="#dataman,precios">⚙️ Precios del café</a>
|
||||||
<a class="button" href="#dataman,config">Ajustes</a>
|
<a class="button" href="#dataman,config">Ajustes</a>
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,44 +1,327 @@
|
|||||||
PAGES.index = {
|
PAGES.index = {
|
||||||
//navcss: "btn1",
|
//navcss: "btn1",
|
||||||
Title: "Inicio",
|
Title: 'Inicio',
|
||||||
icon: "static/appico/house.png",
|
icon: 'static/appico/house.png',
|
||||||
index: function () {
|
index: function () {
|
||||||
container.innerHTML = `
|
var div_stats = safeuuid();
|
||||||
<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>
|
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>
|
||||||
|
<details style="border: 2px solid black; padding: 15px; border-radius: 10px;">
|
||||||
|
<summary>Estadisticas</summary>
|
||||||
|
<div
|
||||||
|
id="${div_stats}"
|
||||||
|
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 15px; margin-bottom: 20px;"
|
||||||
|
></div>
|
||||||
|
</details>
|
||||||
<em>Utiliza el menú superior para abrir un modulo</em>
|
<em>Utiliza el menú superior para abrir un modulo</em>
|
||||||
<br><br>
|
<br /><br />
|
||||||
|
<button class="btn1" onclick="ActualizarProgramaTeleSec()">Actualizar programa</button>
|
||||||
<button class="btn1" onclick="LogOutTeleSec()">Cerrar sesión</button>
|
<button class="btn1" onclick="LogOutTeleSec()">Cerrar sesión</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
if (checkRole('pagos')) {
|
||||||
|
var total_ingresos = safeuuid();
|
||||||
|
var total_gastos = safeuuid();
|
||||||
|
var balance_total = safeuuid();
|
||||||
|
var total_ingresos_srcel = html`
|
||||||
|
<div
|
||||||
|
style="background: linear-gradient(135deg, #2ed573, #26d063); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||||
|
>
|
||||||
|
<span style="font-size: 16px;">Pagos</span><br>
|
||||||
|
<h3 style="margin: 0;">Total Ingresos</h3>
|
||||||
|
<div id="${total_ingresos}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||||
|
0.00€
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
var total_gastos_srcel = html`
|
||||||
|
<div
|
||||||
|
style="background: linear-gradient(135deg, #ff4757, #ff3838); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||||
|
>
|
||||||
|
<span style="font-size: 16px;">Pagos</span><br>
|
||||||
|
<h3 style="margin: 0;">Total Gastos</h3>
|
||||||
|
<div id="${total_gastos}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||||
|
0.00€
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
var balance_total_srcel = html`
|
||||||
|
<div
|
||||||
|
style="background: linear-gradient(135deg, #667eea, #764ba2); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||||
|
>
|
||||||
|
<span style="font-size: 16px;">Pagos</span><br>
|
||||||
|
<h3 style="margin: 0;">Balance Total</h3>
|
||||||
|
<div id="${balance_total}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||||
|
0.00€
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById(div_stats).appendChild(createElementFromHTML(total_ingresos_srcel));
|
||||||
|
document.getElementById(div_stats).appendChild(createElementFromHTML(total_gastos_srcel));
|
||||||
|
document.getElementById(div_stats).appendChild(createElementFromHTML(balance_total_srcel));
|
||||||
|
|
||||||
|
let totalData = {
|
||||||
|
ingresos: {},
|
||||||
|
gastos: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
EventListeners.DB.push(
|
||||||
|
DB.map('pagos', (data, key) => {
|
||||||
|
function applyData(row) {
|
||||||
|
if (!row || typeof row !== 'object') {
|
||||||
|
delete totalData.ingresos[key];
|
||||||
|
delete totalData.gastos[key];
|
||||||
|
} else {
|
||||||
|
const monto = parseFloat(row.Monto || 0) || 0;
|
||||||
|
const tipo = row.Tipo;
|
||||||
|
|
||||||
|
if (tipo === 'Ingreso') {
|
||||||
|
if (row.Origen != 'Promo Bono') {
|
||||||
|
totalData.gastos[key] = 0;
|
||||||
|
totalData.ingresos[key] = monto;
|
||||||
|
}
|
||||||
|
} else if (tipo === 'Gasto') {
|
||||||
|
totalData.ingresos[key] = 0;
|
||||||
|
totalData.gastos[key] = monto;
|
||||||
|
} else {
|
||||||
|
totalData.ingresos[key] = 0;
|
||||||
|
totalData.gastos[key] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalIngresos = Object.values(totalData.ingresos).reduce((a, b) => a + b, 0);
|
||||||
|
const totalGastos = Object.values(totalData.gastos).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
document.getElementById(total_ingresos).innerText = totalIngresos.toFixed(2) + '€';
|
||||||
|
document.getElementById(total_gastos).innerText = totalGastos.toFixed(2) + '€';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data == 'string') {
|
||||||
|
TS_decrypt(data, SECRET, (decoded) => {
|
||||||
|
applyData(decoded);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
applyData(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
EventListeners.Interval.push(
|
||||||
|
setInterval(() => {
|
||||||
|
var balanceReal = 0;
|
||||||
|
Object.values(SC_Personas).forEach((persona) => {
|
||||||
|
balanceReal += parseFloat(persona.Monedero_Balance || 0);
|
||||||
|
});
|
||||||
|
document.getElementById(balance_total).innerText = balanceReal.toFixed(2) + '€';
|
||||||
|
document.getElementById(balance_total).style.color =
|
||||||
|
balanceReal >= 0 ? 'white' : '#ffcccc';
|
||||||
|
}, 1000)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkRole('mensajes')) {
|
||||||
|
var mensajes_sin_leer = safeuuid();
|
||||||
|
var mensajes_sin_leer_srcel = html`
|
||||||
|
<div
|
||||||
|
style="background: linear-gradient(135deg, #66380d, #a5570d); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||||
|
>
|
||||||
|
<span style="font-size: 16px;">Mensajes</span><br>
|
||||||
|
<h3 style="margin: 0;">Sin leer</h3>
|
||||||
|
<div id="${mensajes_sin_leer}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||||
|
0
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById(div_stats).appendChild(createElementFromHTML(mensajes_sin_leer_srcel));
|
||||||
|
|
||||||
|
var unreadById = {};
|
||||||
|
|
||||||
|
EventListeners.DB.push(
|
||||||
|
DB.map('mensajes', (data, key) => {
|
||||||
|
function applyUnread(row) {
|
||||||
|
if (!row || typeof row !== 'object') {
|
||||||
|
delete unreadById[key];
|
||||||
|
} else {
|
||||||
|
var estado = String(row.Estado || '').trim().toLowerCase();
|
||||||
|
var isRead = estado === 'leido' || estado === 'leído';
|
||||||
|
unreadById[key] = isRead ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalUnread = Object.values(unreadById).reduce((a, b) => a + b, 0);
|
||||||
|
document.getElementById(mensajes_sin_leer).innerText = String(totalUnread);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data == 'string') {
|
||||||
|
TS_decrypt(data, SECRET, (decoded) => {
|
||||||
|
applyUnread(decoded);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
applyUnread(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkRole('supercafe')) {
|
||||||
|
var comandas_en_deuda = safeuuid();
|
||||||
|
var comandas_en_deuda_srcel = html`
|
||||||
|
<div
|
||||||
|
style="background: linear-gradient(135deg, #8e44ad, #6c3483); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||||
|
>
|
||||||
|
<span style="font-size: 16px;">SuperCafé</span><br>
|
||||||
|
<h3 style="margin: 0;">Comandas en deuda</h3>
|
||||||
|
<div id="${comandas_en_deuda}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||||
|
0
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById(div_stats).appendChild(createElementFromHTML(comandas_en_deuda_srcel));
|
||||||
|
|
||||||
|
var deudaById = {};
|
||||||
|
|
||||||
|
EventListeners.DB.push(
|
||||||
|
DB.map('supercafe', (data, key) => {
|
||||||
|
function applyDeuda(row) {
|
||||||
|
if (!row || typeof row !== 'object') {
|
||||||
|
delete deudaById[key];
|
||||||
|
} else {
|
||||||
|
var estado = String(row.Estado || '').trim().toLowerCase();
|
||||||
|
deudaById[key] = estado === 'deuda' ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalDeuda = Object.values(deudaById).reduce((a, b) => a + b, 0);
|
||||||
|
document.getElementById(comandas_en_deuda).innerText = String(totalDeuda);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data == 'string') {
|
||||||
|
TS_decrypt(data, SECRET, (decoded) => {
|
||||||
|
applyDeuda(decoded);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
applyDeuda(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkRole('materiales')) {
|
||||||
|
var materiales_comprar = safeuuid();
|
||||||
|
var materiales_revisar = safeuuid();
|
||||||
|
|
||||||
|
var materiales_comprar_srcel = html`
|
||||||
|
<div
|
||||||
|
style="background: linear-gradient(135deg, #e67e22, #d35400); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||||
|
>
|
||||||
|
<span style="font-size: 16px;">Almacén</span><br>
|
||||||
|
<h3 style="margin: 0;">Por comprar</h3>
|
||||||
|
<div id="${materiales_comprar}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||||
|
0
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
var materiales_revisar_srcel = html`
|
||||||
|
<div
|
||||||
|
style="background: linear-gradient(135deg, #2980b9, #1f6391); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||||
|
>
|
||||||
|
<span style="font-size: 16px;">Almacén</span><br>
|
||||||
|
<h3 style="margin: 0;">Por revisar</h3>
|
||||||
|
<div id="${materiales_revisar}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||||
|
0
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById(div_stats).appendChild(createElementFromHTML(materiales_comprar_srcel));
|
||||||
|
document.getElementById(div_stats).appendChild(createElementFromHTML(materiales_revisar_srcel));
|
||||||
|
|
||||||
|
var comprarById = {};
|
||||||
|
var revisarById = {};
|
||||||
|
|
||||||
|
EventListeners.DB.push(
|
||||||
|
DB.map('materiales', (data, key) => {
|
||||||
|
function applyMaterialStats(row) {
|
||||||
|
if (!row || typeof row !== 'object') {
|
||||||
|
delete comprarById[key];
|
||||||
|
delete revisarById[key];
|
||||||
|
} else {
|
||||||
|
var cantidad = parseFloat(row.Cantidad);
|
||||||
|
var cantidadMinima = parseFloat(row.Cantidad_Minima);
|
||||||
|
var lowStock = !isNaN(cantidad) && !isNaN(cantidadMinima) && cantidad < cantidadMinima;
|
||||||
|
comprarById[key] = lowStock ? 1 : 0;
|
||||||
|
|
||||||
|
var revision = String(row.Revision || '?').trim();
|
||||||
|
var needsReview = false;
|
||||||
|
|
||||||
|
if (revision === '?' || revision === '' || revision === '-') {
|
||||||
|
needsReview = true;
|
||||||
|
} else {
|
||||||
|
var match = revision.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
needsReview = true;
|
||||||
|
} else {
|
||||||
|
var y = parseInt(match[1], 10);
|
||||||
|
var m = parseInt(match[2], 10) - 1;
|
||||||
|
var d = parseInt(match[3], 10);
|
||||||
|
var revisionMs = Date.UTC(y, m, d);
|
||||||
|
var now = new Date();
|
||||||
|
var todayMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
||||||
|
var diffDays = Math.floor((todayMs - revisionMs) / 86400000);
|
||||||
|
needsReview = diffDays >= 90;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
revisarById[key] = needsReview ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalComprar = Object.values(comprarById).reduce((a, b) => a + b, 0);
|
||||||
|
var totalRevisar = Object.values(revisarById).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
document.getElementById(materiales_comprar).innerText = String(totalComprar);
|
||||||
|
document.getElementById(materiales_revisar).innerText = String(totalRevisar);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data == 'string') {
|
||||||
|
TS_decrypt(data, SECRET, (decoded) => {
|
||||||
|
applyMaterialStats(decoded);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
applyMaterialStats(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
edit: function (mid) {
|
edit: function (mid) {
|
||||||
switch (mid) {
|
switch (mid) {
|
||||||
case 'qr':
|
case 'qr':
|
||||||
PAGES.index.__scan()
|
PAGES.index.__scan();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
__scan: function (mid) {
|
__scan: function (mid) {
|
||||||
var qrscan = safeuuid()
|
var qrscan = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html` <h1>Escanear Codigo QR</h1>
|
||||||
<h1>Escanear Codigo QR</h1>
|
|
||||||
<div style="max-width: 400px;" id="${qrscan}"></div>
|
<div style="max-width: 400px;" id="${qrscan}"></div>
|
||||||
<br><br>`;
|
<br /><br />`;
|
||||||
var html5QrcodeScanner = new Html5QrcodeScanner(
|
var html5QrcodeScanner = new Html5QrcodeScanner(qrscan, { fps: 10, qrbox: 250 });
|
||||||
qrscan, { fps: 10, qrbox: 250 });
|
|
||||||
|
|
||||||
function onScanSuccess(decodedText, decodedResult) {
|
function onScanSuccess(decodedText, decodedResult) {
|
||||||
html5QrcodeScanner.clear();
|
html5QrcodeScanner.clear();
|
||||||
// Handle on success condition with the decoded text or result.
|
// Handle on success condition with the decoded text or result.
|
||||||
// alert(`Scan result: ${decodedText}`, decodedResult);
|
// alert(`Scan result: ${decodedText}`, decodedResult);
|
||||||
setUrlHash(decodedText)
|
setUrlHash(decodedText);
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
// ^ this will stop the scanner (video feed) and clear the scan area.
|
// ^ this will stop the scanner (video feed) and clear the scan area.
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html5QrcodeScanner.render(onScanSuccess);
|
html5QrcodeScanner.render(onScanSuccess);
|
||||||
EventListeners.QRScanner.push(html5QrcodeScanner)
|
EventListeners.QRScanner.push(html5QrcodeScanner);
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,56 +1,311 @@
|
|||||||
|
function makeCouchURLDisplay(host, user, pass, dbname) {
|
||||||
|
if (!host) return '';
|
||||||
|
var display = user + ':' + pass + '@' + host.replace(/^https?:\/\//, '') + '/' + dbname;
|
||||||
|
return display;
|
||||||
|
}
|
||||||
PAGES.login = {
|
PAGES.login = {
|
||||||
Esconder: true,
|
Esconder: true,
|
||||||
Title: "Login",
|
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>
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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('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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) => {
|
||||||
|
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) {
|
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
|
// Setup form to configure CouchDB remote and initial group/secret
|
||||||
var field_couch = safeuuid();
|
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_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();
|
var btn_save = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Configuración del servidor CouchDB</h1>
|
<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>
|
<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>
|
<fieldset>
|
||||||
<label>Servidor CouchDB (ej: couch.example.com)
|
<label
|
||||||
<input type="text" id="${field_couch}" value="${(localStorage.getItem('TELESEC_COUCH_URL') || '').replace(/^https?:\/\//, '')}"><br><br>
|
>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>
|
||||||
<label>Nombre de la base (opcional, por defecto usa telesec-<grupo>)
|
<label
|
||||||
<input type="text" id="${field_couch_dbname}" value="${localStorage.getItem('TELESEC_COUCH_DBNAME') || ''}"><br><br>
|
>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>
|
</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>
|
<button id="${btn_save}" class="btn5">Guardar y Conectar</button>
|
||||||
|
<button onclick="setUrlHash('login');" class="btn3">Cancelar</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<p>Después de guardar, el navegador intentará sincronizar en segundo plano con el servidor.</p>
|
<p>
|
||||||
|
Después de guardar, el navegador intentará sincronizar en segundo plano con el servidor.
|
||||||
|
</p>
|
||||||
`;
|
`;
|
||||||
// Helper: normalize and apply config object
|
// Helper: normalize and apply config object
|
||||||
function applyConfig(cfg) {
|
function applyConfig(cfg) {
|
||||||
@@ -62,7 +317,9 @@ PAGES.login = {
|
|||||||
var pass = cfg.password || cfg.pass || cfg.p;
|
var pass = cfg.password || cfg.pass || cfg.p;
|
||||||
var secret = (cfg.secret || cfg.key || cfg.secretKey || cfg.SECRET || '').toString();
|
var secret = (cfg.secret || cfg.key || cfg.secretKey || cfg.SECRET || '').toString();
|
||||||
if (!url) throw new Error('Falta campo "server" en JSON');
|
if (!url) throw new Error('Falta campo "server" en JSON');
|
||||||
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + url.replace(/^https?:\/\//, ''));
|
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 (dbname) localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
|
||||||
if (user) localStorage.setItem('TELESEC_COUCH_USER', user);
|
if (user) localStorage.setItem('TELESEC_COUCH_USER', user);
|
||||||
if (pass) localStorage.setItem('TELESEC_COUCH_PASS', pass);
|
if (pass) localStorage.setItem('TELESEC_COUCH_PASS', pass);
|
||||||
@@ -70,134 +327,73 @@ PAGES.login = {
|
|||||||
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
|
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
|
||||||
SECRET = secret.toUpperCase();
|
SECRET = secret.toUpperCase();
|
||||||
}
|
}
|
||||||
DB.init({ secret: SECRET, remoteServer: 'https://' + url.replace(/^https?:\/\//, ''), username: user, password: pass, dbname: dbname || undefined });
|
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');
|
toastr.success('Configuración aplicada e iniciando sincronización');
|
||||||
location.hash = '#login';
|
location.hash = '#login';
|
||||||
setTimeout(function(){ location.reload(); }, 400);
|
setTimeout(function () {
|
||||||
|
location.reload();
|
||||||
|
}, 400);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toastr.error('Error aplicando configuración: ' + (e && e.message ? e.message : e));
|
toastr.error('Error aplicando configuración: ' + (e && e.message ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 = () => {
|
document.getElementById(btn_save).onclick = () => {
|
||||||
var url = document.getElementById(field_couch).value.trim();
|
var url = document.getElementById(field_couch).value.trim();
|
||||||
var dbname = document.getElementById(field_couch_dbname).value.trim();
|
var secret = document.getElementById(field_secret).value.trim();
|
||||||
var user = document.getElementById(field_couch_user).value.trim();
|
var URL_PARSED = parseURL(url);
|
||||||
var pass = document.getElementById(field_couch_pass).value;
|
var host = URL_PARSED.hostname || url;
|
||||||
var secret = document.getElementById(field_secret).value || '';
|
var user = URL_PARSED.username || '';
|
||||||
localStorage.setItem('TELESEC_COUCH_URL', "https://" + url);
|
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_DBNAME', dbname);
|
||||||
localStorage.setItem('TELESEC_COUCH_USER', user);
|
localStorage.setItem('TELESEC_COUCH_USER', user);
|
||||||
localStorage.setItem('TELESEC_COUCH_PASS', pass);
|
localStorage.setItem('TELESEC_COUCH_PASS', pass);
|
||||||
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
|
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
|
||||||
SECRET = secret.toUpperCase();
|
SECRET = secret.toUpperCase();
|
||||||
try {
|
try {
|
||||||
DB.init({ secret: SECRET, remoteServer: "https://" + url, username: user, password: pass, dbname: dbname || undefined });
|
DB.init({
|
||||||
|
secret: SECRET,
|
||||||
|
remoteServer: 'https://' + host,
|
||||||
|
username: user,
|
||||||
|
password: pass,
|
||||||
|
dbname: dbname || undefined,
|
||||||
|
});
|
||||||
toastr.success('Iniciando sincronización con CouchDB');
|
toastr.success('Iniciando sincronización con CouchDB');
|
||||||
location.hash = "#login";
|
location.hash = '#login';
|
||||||
location.reload();
|
//location.reload();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toastr.error('Error al iniciar sincronización: ' + e.message);
|
toastr.error('Error al iniciar sincronización: ' + e.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
index: function (mid) {
|
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 field_persona = safeuuid();
|
||||||
var btn_guardar = safeuuid();
|
var btn_guardar = safeuuid();
|
||||||
var btn_reload = safeuuid();
|
var btn_reload = safeuuid();
|
||||||
var div_actions = safeuuid();
|
var div_actions = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Iniciar sesión</h1>
|
<h1>Iniciar sesión</h1>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Valores</legend>
|
<legend>Valores</legend>
|
||||||
<input type="hidden" id="${field_persona}">
|
<input type="hidden" id="${field_persona}" />
|
||||||
<div id="${div_actions}"></div>
|
<div id="${div_actions}"></div>
|
||||||
<button class="btn5" id="${btn_guardar}">Acceder</button>
|
<button class="btn5" id="${btn_guardar}">Acceder</button>
|
||||||
<button class="btn3" id="${btn_reload}">Recargar lista</button>
|
<button class="btn3" id="${btn_reload}">Recargar lista</button>
|
||||||
@@ -208,33 +404,33 @@ PAGES.login = {
|
|||||||
addCategory_Personas(
|
addCategory_Personas(
|
||||||
divact,
|
divact,
|
||||||
SC_Personas,
|
SC_Personas,
|
||||||
"",
|
'',
|
||||||
(value) => {
|
(value) => {
|
||||||
document.getElementById(field_persona).value = value;
|
document.getElementById(field_persona).value = value;
|
||||||
},
|
},
|
||||||
"¿Quién eres?",
|
'¿Quién eres?',
|
||||||
true,
|
true,
|
||||||
"- Pulsa recargar o rellena los credenciales abajo, si quieres crear un nuevo grupo, pulsa el boton 'Desde cero' -"
|
"- Pulsa recargar o rellena los credenciales abajo, si quieres crear un nuevo grupo, pulsa el boton 'Desde cero' -"
|
||||||
);
|
);
|
||||||
document.getElementById(btn_guardar).onclick = () => {
|
document.getElementById(btn_guardar).onclick = () => {
|
||||||
if (document.getElementById(field_persona).value == "") {
|
if (document.getElementById(field_persona).value == '') {
|
||||||
alert("Tienes que elegir tu cuenta!");
|
alert('Tienes que elegir tu cuenta!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SUB_LOGGED_IN_ID = document.getElementById(field_persona).value
|
SUB_LOGGED_IN_ID = document.getElementById(field_persona).value;
|
||||||
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID]
|
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID];
|
||||||
SUB_LOGGED_IN = true
|
SUB_LOGGED_IN = true;
|
||||||
SetPages()
|
SetPages();
|
||||||
if (location.hash.replace("#", "").startsWith("login")) {
|
if (location.hash.replace('#', '').split("?")[0].startsWith('login')) {
|
||||||
open_page("index");
|
open_page('index');
|
||||||
setUrlHash("index")
|
setUrlHash('index');
|
||||||
} else {
|
} else {
|
||||||
open_page(location.hash.replace("#", ""));
|
open_page(location.hash.replace('#', '').split("?")[0]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById(btn_reload).onclick = () => {
|
document.getElementById(btn_reload).onclick = () => {
|
||||||
open_page("login")
|
open_page('login');
|
||||||
};
|
};
|
||||||
|
|
||||||
// AC_BYPASS: allow creating a local persona from the login screen
|
// AC_BYPASS: allow creating a local persona from the login screen
|
||||||
@@ -242,11 +438,12 @@ PAGES.login = {
|
|||||||
var btn_bypass_create = safeuuid();
|
var btn_bypass_create = safeuuid();
|
||||||
divact.innerHTML += `<button id="${btn_bypass_create}" class="btn2" style="margin-left:10px;">Crear persona local (bypass)</button>`;
|
divact.innerHTML += `<button id="${btn_bypass_create}" class="btn2" style="margin-left:10px;">Crear persona local (bypass)</button>`;
|
||||||
document.getElementById(btn_bypass_create).onclick = () => {
|
document.getElementById(btn_bypass_create).onclick = () => {
|
||||||
var name = prompt("Nombre de la persona (ej: Admin):");
|
var name = prompt('Nombre de la persona (ej: Admin):');
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
var id = 'bypass-' + Date.now();
|
var id = 'bypass-' + Date.now();
|
||||||
var persona = { Nombre: name, Roles: 'ADMIN,' };
|
var persona = { Nombre: name, Roles: 'ADMIN,' };
|
||||||
DB.put('personas', id, persona).then(() => {
|
DB.put('personas', id, persona)
|
||||||
|
.then(() => {
|
||||||
toastr.success('Persona creada: ' + id);
|
toastr.success('Persona creada: ' + id);
|
||||||
localStorage.setItem('TELESEC_BYPASS_ID', id);
|
localStorage.setItem('TELESEC_BYPASS_ID', id);
|
||||||
SUB_LOGGED_IN_ID = id;
|
SUB_LOGGED_IN_ID = id;
|
||||||
@@ -254,10 +451,11 @@ PAGES.login = {
|
|||||||
SUB_LOGGED_IN = true;
|
SUB_LOGGED_IN = true;
|
||||||
SetPages();
|
SetPages();
|
||||||
open_page('index');
|
open_page('index');
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
toastr.error('Error creando persona: ' + (e && e.message ? e.message : e));
|
toastr.error('Error creando persona: ' + (e && e.message ? e.message : e));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
PERMS["materiales"] = "Almacén";
|
PERMS['materiales'] = 'Almacén';
|
||||||
PERMS["materiales:edit"] = "> Editar";
|
PERMS['materiales:edit'] = '> Editar';
|
||||||
PAGES.materiales = {
|
PAGES.materiales = {
|
||||||
navcss: "btn2",
|
navcss: 'btn2',
|
||||||
icon: "static/appico/shelf.png",
|
icon: 'static/appico/shelf.png',
|
||||||
AccessControl: true,
|
AccessControl: true,
|
||||||
Title: "Almacén",
|
Title: 'Almacén',
|
||||||
edit: function (mid) {
|
edit: function (mid) {
|
||||||
if (!checkRole("materiales:edit")) {
|
if (!checkRole('materiales:edit')) {
|
||||||
setUrlHash("materiales");
|
setUrlHash('materiales');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var nameh1 = safeuuid();
|
var nameh1 = safeuuid();
|
||||||
@@ -18,78 +18,30 @@ PAGES.materiales = {
|
|||||||
var field_cantidad_min = safeuuid();
|
var field_cantidad_min = safeuuid();
|
||||||
var field_ubicacion = safeuuid();
|
var field_ubicacion = safeuuid();
|
||||||
var field_notas = safeuuid();
|
var field_notas = safeuuid();
|
||||||
|
var mov_tipo = safeuuid();
|
||||||
|
var mov_cantidad = safeuuid();
|
||||||
|
var mov_nota = safeuuid();
|
||||||
|
var mov_btn = safeuuid();
|
||||||
|
var mov_registro = safeuuid();
|
||||||
|
var mov_chart = safeuuid();
|
||||||
|
var mov_chart_canvas = safeuuid();
|
||||||
|
var mov_open_modal_btn = safeuuid();
|
||||||
|
var btn_print_chart = safeuuid();
|
||||||
|
var mov_modal = safeuuid();
|
||||||
|
var mov_modal_close = safeuuid();
|
||||||
var btn_guardar = safeuuid();
|
var btn_guardar = safeuuid();
|
||||||
var btn_borrar = safeuuid();
|
var btn_borrar = safeuuid();
|
||||||
var FECHA_ISO = new Date().toISOString().split("T")[0];
|
var FECHA_ISO = new Date().toISOString().split('T')[0];
|
||||||
container.innerHTML = `
|
var movimientos = [];
|
||||||
<h1>Material <code id="${nameh1}"></code></h1>
|
var movimientosChartInstance = null;
|
||||||
${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>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Nombre<br>
|
|
||||||
<input type="text" id="${field_nombre}"><br><br>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
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>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
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>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
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 = "") {
|
|
||||||
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"] || "";
|
|
||||||
}
|
|
||||||
if (typeof data == "string") {
|
|
||||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
|
||||||
load_data(data, "%E");
|
|
||||||
}, 'materiales', mid);
|
|
||||||
} else {
|
|
||||||
load_data(data || {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.getElementById(btn_guardar).onclick = () => {
|
|
||||||
// Disable button to prevent double-clicking
|
|
||||||
var guardarBtn = document.getElementById(btn_guardar);
|
|
||||||
if (guardarBtn.disabled) return;
|
|
||||||
|
|
||||||
guardarBtn.disabled = true;
|
function parseNum(v, fallback = 0) {
|
||||||
guardarBtn.style.opacity = "0.5";
|
var n = parseFloat(v);
|
||||||
|
return Number.isFinite(n) ? n : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
var data = {
|
function buildMaterialData() {
|
||||||
|
return {
|
||||||
Nombre: document.getElementById(field_nombre).value,
|
Nombre: document.getElementById(field_nombre).value,
|
||||||
Unidad: document.getElementById(field_unidad).value,
|
Unidad: document.getElementById(field_unidad).value,
|
||||||
Cantidad: document.getElementById(field_cantidad).value,
|
Cantidad: document.getElementById(field_cantidad).value,
|
||||||
@@ -97,49 +49,483 @@ PAGES.materiales = {
|
|||||||
Ubicacion: document.getElementById(field_ubicacion).value,
|
Ubicacion: document.getElementById(field_ubicacion).value,
|
||||||
Revision: document.getElementById(field_revision).value,
|
Revision: document.getElementById(field_revision).value,
|
||||||
Notas: document.getElementById(field_notas).value,
|
Notas: document.getElementById(field_notas).value,
|
||||||
|
Movimientos: movimientos,
|
||||||
};
|
};
|
||||||
document.getElementById("actionStatus").style.display = "block";
|
}
|
||||||
DB.put('materiales', mid, data).then(() => {
|
|
||||||
toastr.success("Guardado!");
|
function renderMovimientos() {
|
||||||
|
var el = document.getElementById(mov_registro);
|
||||||
|
if (!el) return;
|
||||||
|
if (!movimientos.length) {
|
||||||
|
el.innerHTML = '<small>Sin movimientos registrados.</small>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = movimientos
|
||||||
|
.map((mov) => {
|
||||||
|
var fecha = mov.Fecha ? new Date(mov.Fecha).toLocaleString('es-ES') : '-';
|
||||||
|
return html`<tr>
|
||||||
|
<td>${fecha}</td>
|
||||||
|
<td>${mov.Tipo || '-'}</td>
|
||||||
|
<td>${mov.Cantidad ?? '-'}</td>
|
||||||
|
<td>${mov.Antes ?? '-'}</td>
|
||||||
|
<td>${mov.Despues ?? '-'}</td>
|
||||||
|
<td>${mov.Nota || ''}</td>
|
||||||
|
</tr>`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
el.innerHTML = html`
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Cantidad</th>
|
||||||
|
<th>Antes</th>
|
||||||
|
<th>Después</th>
|
||||||
|
<th>Nota</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMovimientosChart() {
|
||||||
|
var el = document.getElementById(mov_chart);
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
if (movimientosChartInstance) {
|
||||||
|
movimientosChartInstance.destroy();
|
||||||
|
movimientosChartInstance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!movimientos.length) {
|
||||||
|
el.innerHTML = html`
|
||||||
|
<h3 style="margin: 0 0 8px 0;">Historial de movimientos por fecha</h3>
|
||||||
|
<small>Sin datos para graficar.</small>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ordered = [...movimientos].sort((a, b) => {
|
||||||
|
return new Date(a.Fecha || 0).getTime() - new Date(b.Fecha || 0).getTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
var deltas = [];
|
||||||
|
var labelsShort = [];
|
||||||
|
|
||||||
|
ordered.forEach((mov) => {
|
||||||
|
var cantidad = parseNum(mov.Cantidad, 0);
|
||||||
|
var delta = 0;
|
||||||
|
if (mov.Tipo === 'Entrada') {
|
||||||
|
delta = cantidad;
|
||||||
|
} else if (mov.Tipo === 'Salida') {
|
||||||
|
delta = -cantidad;
|
||||||
|
} else {
|
||||||
|
var antes = parseNum(mov.Antes, 0);
|
||||||
|
var despues = parseNum(mov.Despues, antes);
|
||||||
|
delta = despues - antes;
|
||||||
|
}
|
||||||
|
deltas.push(Number(delta.toFixed(2)));
|
||||||
|
|
||||||
|
var fechaTxt = mov.Fecha ? new Date(mov.Fecha).toLocaleString('es-ES') : '-';
|
||||||
|
labelsShort.push(fechaTxt);
|
||||||
|
});
|
||||||
|
|
||||||
|
var currentStock = parseNum(document.getElementById(field_cantidad)?.value, 0);
|
||||||
|
var totalNeto = deltas.reduce((acc, n) => acc + n, 0);
|
||||||
|
var stockInicialInferido = currentStock - totalNeto;
|
||||||
|
|
||||||
|
if (ordered.length > 0 && Number.isFinite(parseNum(ordered[0].Antes, NaN))) {
|
||||||
|
stockInicialInferido = parseNum(ordered[0].Antes, stockInicialInferido);
|
||||||
|
}
|
||||||
|
|
||||||
|
var acumulado = stockInicialInferido;
|
||||||
|
var values = deltas.map((neto) => {
|
||||||
|
acumulado += neto;
|
||||||
|
return Number(acumulado.toFixed(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
el.innerHTML = html`
|
||||||
|
<h3 style="margin: 0 0 8px 0;">Historial de movimientos por fecha</h3>
|
||||||
|
<small style="display: block;margin-bottom: 6px;">Stock por fecha (cierre diario)</small>
|
||||||
|
<canvas id="${mov_chart_canvas}" style="width: 100%;height: 280px;"></canvas>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (typeof Chart === 'undefined') {
|
||||||
|
el.innerHTML += '<small>No se pudo cargar la librería de gráficos.</small>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chartCanvasEl = document.getElementById(mov_chart_canvas);
|
||||||
|
if (!chartCanvasEl) return;
|
||||||
|
|
||||||
|
movimientosChartInstance = new Chart(chartCanvasEl, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labelsShort,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Stock diario',
|
||||||
|
data: values,
|
||||||
|
borderColor: '#2d7ef7',
|
||||||
|
backgroundColor: 'rgba(45,126,247,0.16)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.25,
|
||||||
|
pointRadius: 3,
|
||||||
|
pointHoverRadius: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
title: {
|
||||||
|
display: false,
|
||||||
|
text: 'Stock',
|
||||||
|
},
|
||||||
|
grace: '10%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
mode: 'nearest',
|
||||||
|
axis: 'x',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = html`
|
||||||
|
<h1>Material <code id="${nameh1}"></code></h1>
|
||||||
|
<fieldset style="width: 100%;max-width: 980px;box-sizing: border-box;">
|
||||||
|
<div style="display: flex;flex-wrap: wrap;gap: 10px 16px;align-items: flex-end;">
|
||||||
|
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
|
||||||
|
<label for="${field_revision}">Fecha Revisión</label>
|
||||||
|
<input type="date" id="${field_revision}" style="flex: 1;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
|
||||||
|
<label for="${field_nombre}">Nombre</label>
|
||||||
|
<input type="text" id="${field_nombre}" style="flex: 1;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
|
||||||
|
<label for="${field_ubicacion}">Ubicación</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="${field_ubicacion}"
|
||||||
|
value="-"
|
||||||
|
list="${field_ubicacion}_list"
|
||||||
|
style="flex: 1;"
|
||||||
|
/>
|
||||||
|
<datalist id="${field_ubicacion}_list"></datalist>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
|
||||||
|
<label for="${field_unidad}">Unidad</label>
|
||||||
|
<select id="${field_unidad}" style="flex: 1;">
|
||||||
|
<option value="unidad(es)">unidad(es)</option>
|
||||||
|
<option value="paquete(s)">paquete(s)</option>
|
||||||
|
<option value="caja(s)">caja(s)</option>
|
||||||
|
<option value="rollo(s)">rollo(s)</option>
|
||||||
|
<option value="bote(s)">bote(s)</option>
|
||||||
|
|
||||||
|
<option value="metro(s)">metro(s)</option>
|
||||||
|
<option value="litro(s)">litro(s)</option>
|
||||||
|
<option value="kg">kg</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
|
||||||
|
<label for="${field_cantidad}">Cantidad Actual</label>
|
||||||
|
<input type="number" step="0.5" id="${field_cantidad}" style="flex: 1;" disabled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
|
||||||
|
<label for="${field_cantidad_min}">Cantidad Minima</label>
|
||||||
|
<input type="number" step="0.5" id="${field_cantidad_min}" style="flex: 1;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label style="display: flex;flex-direction: column;gap: 6px;min-width: 220px;flex: 1 1 100%;">
|
||||||
|
Notas
|
||||||
|
<textarea id="${field_notas}"></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="${mov_modal}"
|
||||||
|
style="display: none;position: fixed;z-index: 9999;left: 0;top: 0;width: 100%;height: 100%;overflow: auto;background: rgba(0,0,0,0.45);"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="background: #fff;margin: 2vh auto;padding: 14px;border: 1px solid #888;width: min(960px, 96vw);max-height: 94vh;overflow: auto;border-radius: 8px;box-sizing: border-box;"
|
||||||
|
>
|
||||||
|
<div style="display: flex;justify-content: space-between;align-items: center;gap: 10px;">
|
||||||
|
<h3 style="margin: 0;">Realizar movimiento</h3>
|
||||||
|
<button type="button" id="${mov_modal_close}" class="rojo">Cerrar</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 12px;display: flex;gap: 10px;align-items: flex-end;flex-wrap: wrap;">
|
||||||
|
<div style="display: flex;flex-wrap: wrap;gap: 10px 12px;align-items: flex-end;flex: 1 1 420px;">
|
||||||
|
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
|
||||||
|
<label for="${mov_tipo}">Tipo</label>
|
||||||
|
<select id="${mov_tipo}" style="flex: 1;">
|
||||||
|
<option value="Entrada">Entrada</option>
|
||||||
|
<option value="Salida">Salida</option>
|
||||||
|
<option value="Ajuste">Ajuste</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
|
||||||
|
<label for="${mov_cantidad}">Cantidad</label>
|
||||||
|
<input type="number" step="0.5" id="${mov_cantidad}" style="flex: 1;" />
|
||||||
|
</div>
|
||||||
|
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
|
||||||
|
<label for="${mov_nota}">Nota</label>
|
||||||
|
<input type="text" id="${mov_nota}" style="flex: 1;" placeholder="Motivo del movimiento" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex;justify-content: flex-end;flex: 1 1 120px;min-width: 120px;">
|
||||||
|
<button type="button" class="saveico" id="${mov_btn}">
|
||||||
|
<img src="static/floppy_disk_green.png" />
|
||||||
|
<br>Guardar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 style="margin: 14px 0 6px 0;">Registro de movimientos</h4>
|
||||||
|
<div id="${mov_registro}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<button class="saveico" id="${btn_guardar}">
|
||||||
|
<img src="static/floppy_disk_green.png" />
|
||||||
|
<br>Guardar
|
||||||
|
</button>
|
||||||
|
<button class="delico" id="${btn_borrar}">
|
||||||
|
<img src="static/garbage.png" />
|
||||||
|
<br>Borrar
|
||||||
|
</button>
|
||||||
|
<button class="opicon" id="${mov_open_modal_btn}">
|
||||||
|
<img src="static/exchange.png" />
|
||||||
|
<br>Movimientos
|
||||||
|
</button>
|
||||||
|
<button class="opicon" onclick="setUrlHash('materiales')" style="float: right;"> <!-- Align to the right -->
|
||||||
|
<img src="static/exit.png" />
|
||||||
|
<br>Salir
|
||||||
|
</button>
|
||||||
|
<button class="opicon" onclick="window.print()" style="float: right;"> <!-- Align to the right -->
|
||||||
|
<img src="static/printer2.png" />
|
||||||
|
<br>Imprimir
|
||||||
|
</button>
|
||||||
|
</fieldset>
|
||||||
|
<div id="${mov_chart}" style="max-width: 980px;width: 100%;margin-top: 14px;min-height: 260px;height: min(400px, 52vh);"></div>
|
||||||
|
`;
|
||||||
|
// Cargar ubicaciones existentes para autocompletar
|
||||||
|
DB.map('materiales', (data) => {
|
||||||
|
if (!data) return;
|
||||||
|
function addUbicacion(d) {
|
||||||
|
const ubicacion = d.Ubicacion || '-';
|
||||||
|
const datalist = document.getElementById(`${field_ubicacion}_list`);
|
||||||
|
if (!datalist) {
|
||||||
|
console.warn(`Element with ID "${field_ubicacion}_list" not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const optionExists = Array.from(datalist.options).some((opt) => opt.value === ubicacion);
|
||||||
|
if (!optionExists) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = ubicacion;
|
||||||
|
datalist.appendChild(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
TS_decrypt(
|
||||||
|
data,
|
||||||
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
addUbicacion(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'materiales',
|
||||||
|
mid
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
addUbicacion(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cargar datos del material
|
||||||
|
DB.get('materiales', mid).then((data) => {
|
||||||
|
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'] || '';
|
||||||
|
movimientos = Array.isArray(data['Movimientos']) ? data['Movimientos'] : [];
|
||||||
|
renderMovimientos();
|
||||||
|
renderMovimientosChart();
|
||||||
|
}
|
||||||
|
if (typeof data == 'string') {
|
||||||
|
TS_decrypt(
|
||||||
|
data,
|
||||||
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
|
load_data(data, '%E');
|
||||||
|
},
|
||||||
|
'materiales',
|
||||||
|
mid
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
load_data(data || {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById(mov_open_modal_btn).onclick = () => {
|
||||||
|
document.getElementById(mov_modal).style.display = 'block';
|
||||||
|
renderMovimientos();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById(mov_modal_close).onclick = () => {
|
||||||
|
document.getElementById(mov_modal).style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById(mov_modal).onclick = (evt) => {
|
||||||
|
if (evt.target.id === mov_modal) {
|
||||||
|
document.getElementById(mov_modal).style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById(mov_btn).onclick = () => {
|
||||||
|
var btn = document.getElementById(mov_btn);
|
||||||
|
if (btn.disabled) return;
|
||||||
|
|
||||||
|
var tipo = document.getElementById(mov_tipo).value;
|
||||||
|
var cantidadMov = parseNum(document.getElementById(mov_cantidad).value, NaN);
|
||||||
|
var nota = document.getElementById(mov_nota).value || '';
|
||||||
|
var actual = parseNum(document.getElementById(field_cantidad).value, 0);
|
||||||
|
|
||||||
|
if (!Number.isFinite(cantidadMov) || cantidadMov <= 0) {
|
||||||
|
toastr.warning('Indica una cantidad válida para el movimiento');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nuevaCantidad = actual;
|
||||||
|
if (tipo === 'Entrada') {
|
||||||
|
nuevaCantidad = actual + cantidadMov;
|
||||||
|
} else if (tipo === 'Salida') {
|
||||||
|
nuevaCantidad = actual - cantidadMov;
|
||||||
|
} else if (tipo === 'Ajuste') {
|
||||||
|
nuevaCantidad = cantidadMov;
|
||||||
|
}
|
||||||
|
|
||||||
|
movimientos.unshift({
|
||||||
|
Fecha: new Date().toISOString(),
|
||||||
|
Tipo: tipo,
|
||||||
|
Cantidad: cantidadMov,
|
||||||
|
Antes: actual,
|
||||||
|
Despues: nuevaCantidad,
|
||||||
|
Nota: nota,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById(field_cantidad).value = nuevaCantidad;
|
||||||
|
document.getElementById(field_revision).value = FECHA_ISO;
|
||||||
|
document.getElementById(mov_cantidad).value = '';
|
||||||
|
document.getElementById(mov_nota).value = '';
|
||||||
|
renderMovimientos();
|
||||||
|
renderMovimientosChart();
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.style.opacity = '0.5';
|
||||||
|
document.getElementById('actionStatus').style.display = 'block';
|
||||||
|
DB.put('materiales', mid, buildMaterialData())
|
||||||
|
.then(() => {
|
||||||
|
toastr.success('Movimiento registrado');
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('DB.put error', e);
|
||||||
|
toastr.error('Error al guardar el movimiento');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.style.opacity = '1';
|
||||||
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = buildMaterialData();
|
||||||
|
document.getElementById('actionStatus').style.display = 'block';
|
||||||
|
DB.put('materiales', mid, data)
|
||||||
|
.then(() => {
|
||||||
|
toastr.success('Guardado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
setUrlHash("materiales");
|
setUrlHash('materiales');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
console.warn('DB.put error', e);
|
console.warn('DB.put error', e);
|
||||||
guardarBtn.disabled = false;
|
guardarBtn.disabled = false;
|
||||||
guardarBtn.style.opacity = "1";
|
guardarBtn.style.opacity = '1';
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
toastr.error("Error al guardar el material");
|
toastr.error('Error al guardar el material');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
document.getElementById(btn_borrar).onclick = () => {
|
document.getElementById(btn_borrar).onclick = () => {
|
||||||
if (confirm("¿Quieres borrar este material?") == true) {
|
if (confirm('¿Quieres borrar este material?') == true) {
|
||||||
DB.del('materiales', mid).then(() => {
|
DB.del('materiales', mid).then(() => {
|
||||||
toastr.error("Borrado!");
|
toastr.error('Borrado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setUrlHash("materiales");
|
setUrlHash('materiales');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
index: function () {
|
index: function () {
|
||||||
if (!checkRole("materiales")) {
|
if (!checkRole('materiales')) {
|
||||||
setUrlHash("index");
|
setUrlHash('index');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var btn_new = safeuuid();
|
var btn_new = safeuuid();
|
||||||
var select_ubicacion = safeuuid();
|
var select_ubicacion = safeuuid();
|
||||||
var check_lowstock = safeuuid();
|
var check_lowstock = safeuuid();
|
||||||
var tableContainer = safeuuid();
|
var tableContainer = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Materiales del Almacén</h1>
|
<h1>Materiales del Almacén</h1>
|
||||||
<label>
|
<label>
|
||||||
<b>Solo lo que falta:</b>
|
<b>Solo lo que falta:</b>
|
||||||
<input type="checkbox" id="${check_lowstock}" style="height: 25px;width: 25px;">
|
<input type="checkbox" id="${check_lowstock}" style="height: 25px;width: 25px;" /> </label
|
||||||
</label><br>
|
><br />
|
||||||
<label>Filtrar por ubicación:
|
<label
|
||||||
|
>Filtrar por ubicación:
|
||||||
<select id="${select_ubicacion}">
|
<select id="${select_ubicacion}">
|
||||||
<option value="">(Todas)</option>
|
<option value="">(Todas)</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -149,33 +535,31 @@ PAGES.materiales = {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const config = [
|
const config = [
|
||||||
{ key: "Revision", label: "Ult. Revisión", type: "fecha-diff", default: "" },
|
{ key: 'Revision', label: 'Ult. Revisión', type: 'fecha-diff', default: '' },
|
||||||
{ key: "Nombre", label: "Nombre", type: "text", default: "" },
|
{ key: 'Nombre', label: 'Nombre', type: 'text', default: '' },
|
||||||
{ key: "Ubicacion", label: "Ubicación", type: "text", default: "--" },
|
{ key: 'Ubicacion', label: 'Ubicación', type: 'text', default: '--' },
|
||||||
{
|
{
|
||||||
key: "Cantidad",
|
key: 'Cantidad',
|
||||||
label: "Cantidad",
|
label: 'Cantidad',
|
||||||
type: "template",
|
type: 'template',
|
||||||
template: (data, element) => {
|
template: (data, element) => {
|
||||||
const min = parseFloat(data.Cantidad_Minima);
|
const min = parseFloat(data.Cantidad_Minima);
|
||||||
const act = parseFloat(data.Cantidad);
|
const act = parseFloat(data.Cantidad);
|
||||||
const sma = act < min ? `<small>- min. ${data.Cantidad_Minima || "?"}</small>` : ""
|
const sma = act < min ? `<small>- min. ${data.Cantidad_Minima || '?'}</small>` : '';
|
||||||
element.innerHTML = `${data.Cantidad || "?"} ${
|
element.innerHTML = html`${data.Cantidad || '?'} ${data.Unidad || '?'} ${sma}`;
|
||||||
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
|
// Obtener todas las ubicaciones únicas y poblar el <select>, desencriptando si es necesario
|
||||||
DB.map("materiales", (data, key) => {
|
DB.map('materiales', (data, key) => {
|
||||||
try {
|
try {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
function addUbicacion(d) {
|
function addUbicacion(d) {
|
||||||
const ubicacion = d.Ubicacion || "-";
|
const ubicacion = d.Ubicacion || '-';
|
||||||
const select = document.getElementById(select_ubicacion);
|
const select = document.getElementById(select_ubicacion);
|
||||||
|
|
||||||
if (!select) {
|
if (!select) {
|
||||||
@@ -183,50 +567,54 @@ PAGES.materiales = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionExists = Array.from(select.options).some(
|
const optionExists = Array.from(select.options).some((opt) => opt.value === ubicacion);
|
||||||
(opt) => opt.value === ubicacion
|
|
||||||
);
|
|
||||||
if (!optionExists) {
|
if (!optionExists) {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement('option');
|
||||||
option.value = ubicacion;
|
option.value = ubicacion;
|
||||||
option.textContent = ubicacion;
|
option.textContent = ubicacion;
|
||||||
select.appendChild(option);
|
select.appendChild(option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof data === "string") {
|
if (typeof data === 'string') {
|
||||||
TS_decrypt(data, SECRET, (dec, wasEncrypted) => {
|
TS_decrypt(
|
||||||
if (dec && typeof dec === "object") {
|
data,
|
||||||
|
SECRET,
|
||||||
|
(dec, wasEncrypted) => {
|
||||||
|
if (dec && typeof dec === 'object') {
|
||||||
addUbicacion(dec);
|
addUbicacion(dec);
|
||||||
}
|
}
|
||||||
}, 'materiales', key);
|
},
|
||||||
|
'materiales',
|
||||||
|
key
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
addUbicacion(data);
|
addUbicacion(data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Error processing ubicacion:", error);
|
console.warn('Error processing ubicacion:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Función para renderizar la tabla filtrada
|
// Función para renderizar la tabla filtrada
|
||||||
function renderTable(filtroUbicacion) {
|
function renderTable(filtroUbicacion) {
|
||||||
TS_IndexElement(
|
TS_IndexElement(
|
||||||
"materiales",
|
'materiales',
|
||||||
config,
|
config,
|
||||||
"materiales",
|
'materiales',
|
||||||
document.getElementById(tableContainer),
|
document.getElementById(tableContainer),
|
||||||
function (data, new_tr) {
|
function (data, new_tr) {
|
||||||
if (parseFloat(data.Cantidad) < parseFloat(data.Cantidad_Minima)) {
|
if (parseFloat(data.Cantidad) < parseFloat(data.Cantidad_Minima)) {
|
||||||
new_tr.style.background = "#fcfcb0";
|
new_tr.style.background = '#fcfcb0';
|
||||||
}
|
}
|
||||||
if (parseFloat(data.Cantidad) <= 0) {
|
if (parseFloat(data.Cantidad) <= 0) {
|
||||||
new_tr.style.background = "#ffc0c0";
|
new_tr.style.background = '#ffc0c0';
|
||||||
}
|
}
|
||||||
if ((data.Cantidad || "?") == "?") {
|
if ((data.Cantidad || '?') == '?') {
|
||||||
new_tr.style.background = "#d0d0ff";
|
new_tr.style.background = '#d0d0ff';
|
||||||
}
|
}
|
||||||
if ((data.Revision || "?") == "?") {
|
if ((data.Revision || '?') == '?') {
|
||||||
new_tr.style.background = "#d0d0ff";
|
new_tr.style.background = '#d0d0ff';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function (data) {
|
function (data) {
|
||||||
@@ -234,8 +622,7 @@ PAGES.materiales = {
|
|||||||
!document.getElementById(check_lowstock).checked ||
|
!document.getElementById(check_lowstock).checked ||
|
||||||
parseFloat(data.Cantidad) < parseFloat(data.Cantidad_Minima);
|
parseFloat(data.Cantidad) < parseFloat(data.Cantidad_Minima);
|
||||||
|
|
||||||
var is_region =
|
var is_region = filtroUbicacion === '' || data.Ubicacion === filtroUbicacion;
|
||||||
filtroUbicacion === "" || data.Ubicacion === filtroUbicacion;
|
|
||||||
|
|
||||||
return !(is_low_stock && is_region);
|
return !(is_low_stock && is_region);
|
||||||
}
|
}
|
||||||
@@ -243,7 +630,7 @@ PAGES.materiales = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inicializar tabla sin filtro
|
// Inicializar tabla sin filtro
|
||||||
renderTable("");
|
renderTable('');
|
||||||
|
|
||||||
// Evento para filtrar por ubicación
|
// Evento para filtrar por ubicación
|
||||||
document.getElementById(select_ubicacion).onchange = function () {
|
document.getElementById(select_ubicacion).onchange = function () {
|
||||||
@@ -254,11 +641,11 @@ PAGES.materiales = {
|
|||||||
renderTable(document.getElementById(select_ubicacion).value);
|
renderTable(document.getElementById(select_ubicacion).value);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!checkRole("materiales:edit")) {
|
if (!checkRole('materiales:edit')) {
|
||||||
document.getElementById(btn_new).style.display = "none";
|
document.getElementById(btn_new).style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
document.getElementById(btn_new).onclick = () => {
|
document.getElementById(btn_new).onclick = () => {
|
||||||
setUrlHash("materiales," + safeuuid(""));
|
setUrlHash('materiales,' + safeuuid(''));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
294
src/page/mensajes.js
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
PERMS['mensajes'] = 'Mensajes';
|
||||||
|
PERMS['mensajes:edit'] = '> Editar';
|
||||||
|
PAGES.mensajes = {
|
||||||
|
navcss: 'btn5',
|
||||||
|
icon: 'static/appico/message.png',
|
||||||
|
AccessControl: true,
|
||||||
|
// AccessControlRole is not needed.
|
||||||
|
Title: 'Mensajes',
|
||||||
|
edit: function (mid) {
|
||||||
|
if (!checkRole('mensajes:edit')) {
|
||||||
|
setUrlHash('mensajes');
|
||||||
|
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>Mensaje <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>
|
||||||
|
<label>
|
||||||
|
Origen<br />
|
||||||
|
<input type="text" id="${field_autor}" value="" /><br /><br />
|
||||||
|
</label>
|
||||||
|
</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="saveico" id="${btn_guardar}">
|
||||||
|
<img src="static/floppy_disk_green.png" />
|
||||||
|
<br>Guardar
|
||||||
|
</button>
|
||||||
|
<button class="delico" id="${btn_borrar}">
|
||||||
|
<img src="static/garbage.png" />
|
||||||
|
<br>Borrar
|
||||||
|
</button>
|
||||||
|
<button class="opicon" onclick="setUrlHash('mensajes')" style="float: right;"> <!-- Align to the right -->
|
||||||
|
<img src="static/exit.png" />
|
||||||
|
<br>Salir
|
||||||
|
</button>
|
||||||
|
<button class="opicon" onclick="window.print()" style="float: right;"> <!-- Align to the right -->
|
||||||
|
<img src="static/printer2.png" />
|
||||||
|
<br>Imprimir
|
||||||
|
</button>
|
||||||
|
</fieldset>
|
||||||
|
`;
|
||||||
|
DB.get('mensajes', 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_DETAILS["Nombre"] || '';
|
||||||
|
|
||||||
|
// 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('mensajes', 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');
|
||||||
|
});
|
||||||
|
} 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('mensajes', 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,
|
||||||
|
};
|
||||||
|
document.getElementById('actionStatus').style.display = 'block';
|
||||||
|
DB.put('mensajes', mid, data)
|
||||||
|
.then(() => {
|
||||||
|
// subir attachments si los hay
|
||||||
|
const uploadPromises = [];
|
||||||
|
attachmentsToUpload.forEach((att) => {
|
||||||
|
if (DB.putAttachment) {
|
||||||
|
uploadPromises.push(
|
||||||
|
DB.putAttachment('mensajes', 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 = 'mensajes:' + 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('mensajes', mid, name)
|
||||||
|
.then((durl) => {
|
||||||
|
addAttachmentRow(name, durl);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* ignore reload errors */
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
toastr.success('Guardado!');
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
|
setUrlHash('mensajes');
|
||||||
|
}, 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 el mensaje');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
document.getElementById(btn_borrar).onclick = () => {
|
||||||
|
if (confirm('¿Quieres borrar este mensaje?') == true) {
|
||||||
|
DB.del('mensajes', mid).then(() => {
|
||||||
|
toastr.error('Borrado!');
|
||||||
|
setTimeout(() => {
|
||||||
|
setUrlHash('mensajes');
|
||||||
|
}, SAVE_WAIT);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
index: function () {
|
||||||
|
if (!checkRole('mensajes')) {
|
||||||
|
setUrlHash('index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tablebody = safeuuid();
|
||||||
|
var btn_new = safeuuid();
|
||||||
|
container.innerHTML = html`
|
||||||
|
<h1>Mensajes</h1>
|
||||||
|
<button id="${btn_new}">Nuevo mensaje</button>
|
||||||
|
<div id="cont"></div>
|
||||||
|
`;
|
||||||
|
TS_IndexElement(
|
||||||
|
'mensajes',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: 'Autor',
|
||||||
|
type: 'raw',
|
||||||
|
default: '',
|
||||||
|
label: 'Origen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Asunto',
|
||||||
|
type: 'raw',
|
||||||
|
default: '',
|
||||||
|
label: 'Asunto',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'mensajes',
|
||||||
|
document.querySelector('#cont')
|
||||||
|
);
|
||||||
|
if (!checkRole('mensajes:edit')) {
|
||||||
|
document.getElementById(btn_new).style.display = 'none';
|
||||||
|
} else {
|
||||||
|
document.getElementById(btn_new).onclick = () => {
|
||||||
|
setUrlHash('mensajes,' + safeuuid(''));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
PERMS["notas"] = "Notas"
|
PERMS['notas'] = 'Notas';
|
||||||
PERMS["notas:edit"] = "> Editar"
|
PERMS['notas:edit'] = '> Editar';
|
||||||
PAGES.notas = {
|
PAGES.notas = {
|
||||||
navcss: "btn5",
|
navcss: 'btn5',
|
||||||
icon: "static/appico/edit.png",
|
icon: 'static/appico/edit.png',
|
||||||
AccessControl: true,
|
AccessControl: true,
|
||||||
Title: "Notas",
|
Title: 'Notas',
|
||||||
edit: function (mid) {
|
edit: function (mid) {
|
||||||
if (!checkRole("notas:edit")) {setUrlHash("notas");return}
|
if (!checkRole('notas:edit')) {
|
||||||
|
setUrlHash('notas');
|
||||||
|
return;
|
||||||
|
}
|
||||||
var nameh1 = safeuuid();
|
var nameh1 = safeuuid();
|
||||||
var field_asunto = safeuuid();
|
var field_asunto = safeuuid();
|
||||||
var field_contenido = safeuuid();
|
var field_contenido = safeuuid();
|
||||||
@@ -16,30 +19,48 @@ PAGES.notas = {
|
|||||||
var btn_guardar = safeuuid();
|
var btn_guardar = safeuuid();
|
||||||
var btn_borrar = safeuuid();
|
var btn_borrar = safeuuid();
|
||||||
var div_actions = safeuuid();
|
var div_actions = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Nota <code id="${nameh1}"></code></h1>
|
<h1>Nota <code id="${nameh1}"></code></h1>
|
||||||
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
|
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
|
||||||
<legend>Valores</legend>
|
<legend>Valores</legend>
|
||||||
<div style="max-width: 400px;">
|
<div style="max-width: 400px;">
|
||||||
<label>
|
<label>
|
||||||
Asunto<br>
|
Asunto<br />
|
||||||
<input type="text" id="${field_asunto}" value=""><br><br>
|
<input type="text" id="${field_asunto}" value="" /><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<input type="hidden" id="${field_autor}" value="">
|
<input type="hidden" id="${field_autor}" value="" />
|
||||||
<div id="${div_actions}"></div>
|
<div id="${div_actions}"></div>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
Contenido<br>
|
Contenido<br />
|
||||||
<textarea id="${field_contenido}" style="width: calc(100% - 15px); height: 400px;"></textarea><br><br>
|
<textarea
|
||||||
|
id="${field_contenido}"
|
||||||
|
style="width: calc(100% - 15px); height: 400px;"
|
||||||
|
></textarea
|
||||||
|
><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Adjuntos (Fotos o archivos)<br>
|
Adjuntos (Fotos o archivos)<br />
|
||||||
<input type="file" id="${field_files}" multiple><br><br>
|
<input type="file" id="${field_files}" multiple /><br /><br />
|
||||||
<div id="${attachments_list}"></div>
|
<div id="${attachments_list}"></div>
|
||||||
</label>
|
</label>
|
||||||
<hr>
|
<hr />
|
||||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
<button class="saveico" id="${btn_guardar}">
|
||||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
<img src="static/floppy_disk_green.png" />
|
||||||
|
<br>Guardar
|
||||||
|
</button>
|
||||||
|
<button class="delico" id="${btn_borrar}">
|
||||||
|
<img src="static/garbage.png" />
|
||||||
|
<br>Borrar
|
||||||
|
</button>
|
||||||
|
<button class="opicon" onclick="setUrlHash('notas')" style="float: right;"> <!-- Align to the right -->
|
||||||
|
<img src="static/exit.png" />
|
||||||
|
<br>Salir
|
||||||
|
</button>
|
||||||
|
<button class="opicon" onclick="window.print()" style="float: right;"> <!-- Align to the right -->
|
||||||
|
<img src="static/printer2.png" />
|
||||||
|
<br>Imprimir
|
||||||
|
</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
var divact = document.getElementById(div_actions);
|
var divact = document.getElementById(div_actions);
|
||||||
@@ -50,42 +71,45 @@ PAGES.notas = {
|
|||||||
(value) => {
|
(value) => {
|
||||||
document.getElementById(field_autor).value = value;
|
document.getElementById(field_autor).value = value;
|
||||||
},
|
},
|
||||||
"Autor"
|
'Autor'
|
||||||
);
|
);
|
||||||
DB.get('notas', mid).then((data) => {
|
DB.get('notas', mid).then((data) => {
|
||||||
function load_data(data, ENC = "") {
|
function load_data(data, ENC = '') {
|
||||||
document.getElementById(nameh1).innerText = mid;
|
document.getElementById(nameh1).innerText = mid;
|
||||||
document.getElementById(field_asunto).value = data["Asunto"] || "";
|
document.getElementById(field_asunto).value = data['Asunto'] || '';
|
||||||
document.getElementById(field_contenido).value =
|
document.getElementById(field_contenido).value = data['Contenido'] || '';
|
||||||
data["Contenido"] || "";
|
document.getElementById(field_autor).value = data['Autor'] || SUB_LOGGED_IN_ID || '';
|
||||||
document.getElementById(field_autor).value = data["Autor"] || SUB_LOGGED_IN_ID || "";
|
|
||||||
|
|
||||||
// Persona select
|
// Persona select
|
||||||
divact.innerHTML = "";
|
divact.innerHTML = '';
|
||||||
addCategory_Personas(
|
addCategory_Personas(
|
||||||
divact,
|
divact,
|
||||||
SC_Personas,
|
SC_Personas,
|
||||||
data["Autor"] || SUB_LOGGED_IN_ID || "",
|
data['Autor'] || SUB_LOGGED_IN_ID || '',
|
||||||
(value) => {
|
(value) => {
|
||||||
document.getElementById(field_autor).value = value;
|
document.getElementById(field_autor).value = value;
|
||||||
},
|
},
|
||||||
"Autor"
|
'Autor'
|
||||||
);
|
);
|
||||||
// Mostrar adjuntos existentes (si los hay).
|
// Mostrar adjuntos existentes (si los hay).
|
||||||
// No confiar en `data._attachments` porque `DB.get` devuelve solo `doc.data`.
|
// No confiar en `data._attachments` porque `DB.get` devuelve solo `doc.data`.
|
||||||
const attachContainer = document.getElementById(attachments_list);
|
const attachContainer = document.getElementById(attachments_list);
|
||||||
attachContainer.innerHTML = "";
|
attachContainer.innerHTML = '';
|
||||||
// Usar API de DB para listar attachments (no acceder a internals desde la UI)
|
// Usar API de DB para listar attachments (no acceder a internals desde la UI)
|
||||||
DB.listAttachments('notas', mid).then((list) => {
|
DB.listAttachments('notas', mid)
|
||||||
|
.then((list) => {
|
||||||
if (!list || !Array.isArray(list)) return;
|
if (!list || !Array.isArray(list)) return;
|
||||||
list.forEach((att) => {
|
list.forEach((att) => {
|
||||||
addAttachmentRow(att.name, att.dataUrl);
|
addAttachmentRow(att.name, att.dataUrl);
|
||||||
});
|
});
|
||||||
}).catch((e) => { console.warn('listAttachments error', e); });
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('listAttachments error', e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data) => {
|
TS_decrypt(data, SECRET, (data) => {
|
||||||
load_data(data, "%E");
|
load_data(data, '%E');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
load_data(data || {});
|
load_data(data || {});
|
||||||
@@ -97,7 +121,9 @@ PAGES.notas = {
|
|||||||
const attachContainer = document.getElementById(attachments_list);
|
const attachContainer = document.getElementById(attachments_list);
|
||||||
const idRow = safeuuid();
|
const idRow = safeuuid();
|
||||||
const isImage = url && url.indexOf('data:image') === 0;
|
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 preview = isImage
|
||||||
|
? `<img src="${url}" height="80" style="margin-right:8px;">`
|
||||||
|
: `<a href="${url}" target="_blank">${name}</a>`;
|
||||||
const html = `
|
const html = `
|
||||||
<div id="${idRow}" style="display:flex;align-items:center;margin:6px 0;border:1px solid #ddd;padding:6px;border-radius:6px;">
|
<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 style="flex:1">${preview}<strong style="margin-left:8px">${name}</strong></div>
|
||||||
@@ -108,14 +134,19 @@ PAGES.notas = {
|
|||||||
btn.onclick = () => {
|
btn.onclick = () => {
|
||||||
if (!confirm('¿Borrar este adjunto?')) return;
|
if (!confirm('¿Borrar este adjunto?')) return;
|
||||||
// Usar API pública en DB para borrar metadata del attachment
|
// Usar API pública en DB para borrar metadata del attachment
|
||||||
DB.deleteAttachment('notas', mid, name).then((ok) => {
|
DB.deleteAttachment('notas', mid, name)
|
||||||
|
.then((ok) => {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
document.getElementById(idRow).remove();
|
document.getElementById(idRow).remove();
|
||||||
toastr.error('Adjunto borrado');
|
toastr.error('Adjunto borrado');
|
||||||
} else {
|
} else {
|
||||||
toastr.error('No se pudo borrar el adjunto');
|
toastr.error('No se pudo borrar el adjunto');
|
||||||
}
|
}
|
||||||
}).catch((e) => { console.warn('deleteAttachment error', e); toastr.error('Error borrando adjunto'); });
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('deleteAttachment error', e);
|
||||||
|
toastr.error('Error borrando adjunto');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -126,7 +157,11 @@ PAGES.notas = {
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function (ev) {
|
reader.onload = function (ev) {
|
||||||
const dataUrl = ev.target.result;
|
const dataUrl = ev.target.result;
|
||||||
attachmentsToUpload.push({ name: file.name, data: dataUrl, type: file.type || 'application/octet-stream' });
|
attachmentsToUpload.push({
|
||||||
|
name: file.name,
|
||||||
|
data: dataUrl,
|
||||||
|
type: file.type || 'application/octet-stream',
|
||||||
|
});
|
||||||
// mostrar preview temporal
|
// mostrar preview temporal
|
||||||
addAttachmentRow(file.name, dataUrl);
|
addAttachmentRow(file.name, dataUrl);
|
||||||
};
|
};
|
||||||
@@ -141,29 +176,38 @@ PAGES.notas = {
|
|||||||
if (guardarBtn.disabled) return;
|
if (guardarBtn.disabled) return;
|
||||||
|
|
||||||
guardarBtn.disabled = true;
|
guardarBtn.disabled = true;
|
||||||
guardarBtn.style.opacity = "0.5";
|
guardarBtn.style.opacity = '0.5';
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
Autor: document.getElementById(field_autor).value,
|
Autor: document.getElementById(field_autor).value,
|
||||||
Contenido: document.getElementById(field_contenido).value,
|
Contenido: document.getElementById(field_contenido).value,
|
||||||
Asunto: document.getElementById(field_asunto).value,
|
Asunto: document.getElementById(field_asunto).value,
|
||||||
};
|
};
|
||||||
document.getElementById("actionStatus").style.display = "block";
|
document.getElementById('actionStatus').style.display = 'block';
|
||||||
DB.put('notas', mid, data).then(() => {
|
DB.put('notas', mid, data)
|
||||||
|
.then(() => {
|
||||||
// subir attachments si los hay
|
// subir attachments si los hay
|
||||||
const uploadPromises = [];
|
const uploadPromises = [];
|
||||||
attachmentsToUpload.forEach((att) => {
|
attachmentsToUpload.forEach((att) => {
|
||||||
if (DB.putAttachment) {
|
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(() => {
|
Promise.all(uploadPromises)
|
||||||
|
.then(() => {
|
||||||
// limpiar lista temporal y recargar attachments
|
// limpiar lista temporal y recargar attachments
|
||||||
attachmentsToUpload.length = 0;
|
attachmentsToUpload.length = 0;
|
||||||
try { // recargar lista actual sin salir
|
try {
|
||||||
|
// recargar lista actual sin salir
|
||||||
const pouchId = 'notas:' + mid;
|
const pouchId = 'notas:' + mid;
|
||||||
if (DB && DB._internal && DB._internal.local) {
|
if (DB && DB._internal && DB._internal.local) {
|
||||||
DB._internal.local.get(pouchId, { attachments: true }).then((doc) => {
|
DB._internal.local
|
||||||
|
.get(pouchId, { attachments: true })
|
||||||
|
.then((doc) => {
|
||||||
const attachContainer = document.getElementById(attachments_list);
|
const attachContainer = document.getElementById(attachments_list);
|
||||||
attachContainer.innerHTML = '';
|
attachContainer.innerHTML = '';
|
||||||
if (doc && doc._attachments) {
|
if (doc && doc._attachments) {
|
||||||
@@ -171,82 +215,98 @@ PAGES.notas = {
|
|||||||
try {
|
try {
|
||||||
const att = doc._attachments[name];
|
const att = doc._attachments[name];
|
||||||
if (att && att.data) {
|
if (att && att.data) {
|
||||||
const durl = 'data:' + (att.content_type || 'application/octet-stream') + ';base64,' + att.data;
|
const durl =
|
||||||
|
'data:' +
|
||||||
|
(att.content_type || 'application/octet-stream') +
|
||||||
|
';base64,' +
|
||||||
|
att.data;
|
||||||
addAttachmentRow(name, durl);
|
addAttachmentRow(name, durl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
DB.getAttachment('notas', mid, name).then((durl) => { addAttachmentRow(name, durl); }).catch(() => {});
|
DB.getAttachment('notas', mid, name)
|
||||||
|
.then((durl) => {
|
||||||
|
addAttachmentRow(name, durl);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(() => { /* ignore reload errors */ });
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* ignore reload errors */
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
toastr.success("Guardado!");
|
toastr.success('Guardado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
setUrlHash("notas");
|
setUrlHash('notas');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
console.warn('Attachment upload error', e);
|
console.warn('Attachment upload error', e);
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
guardarBtn.disabled = false;
|
guardarBtn.disabled = false;
|
||||||
guardarBtn.style.opacity = "1";
|
guardarBtn.style.opacity = '1';
|
||||||
toastr.error("Error al guardar los adjuntos");
|
toastr.error('Error al guardar los adjuntos');
|
||||||
});
|
});
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
console.warn('DB.put error', e);
|
console.warn('DB.put error', e);
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
guardarBtn.disabled = false;
|
guardarBtn.disabled = false;
|
||||||
guardarBtn.style.opacity = "1";
|
guardarBtn.style.opacity = '1';
|
||||||
toastr.error("Error al guardar la nota");
|
toastr.error('Error al guardar la nota');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
document.getElementById(btn_borrar).onclick = () => {
|
document.getElementById(btn_borrar).onclick = () => {
|
||||||
if (confirm("¿Quieres borrar esta nota?") == true) {
|
if (confirm('¿Quieres borrar esta nota?') == true) {
|
||||||
DB.del('notas', mid).then(() => {
|
DB.del('notas', mid).then(() => {
|
||||||
toastr.error("Borrado!");
|
toastr.error('Borrado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setUrlHash("notas");
|
setUrlHash('notas');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
index: function () {
|
index: function () {
|
||||||
if (!checkRole("notas")) {setUrlHash("index");return}
|
if (!checkRole('notas')) {
|
||||||
|
setUrlHash('index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const tablebody = safeuuid();
|
const tablebody = safeuuid();
|
||||||
var btn_new = safeuuid();
|
var btn_new = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Notas</h1>
|
<h1>Notas</h1>
|
||||||
<button id="${btn_new}">Nueva nota</button>
|
<button id="${btn_new}">Nueva nota</button>
|
||||||
<div id="cont"></div>
|
<div id="cont"></div>
|
||||||
`;
|
`;
|
||||||
TS_IndexElement(
|
TS_IndexElement(
|
||||||
"notas",
|
'notas',
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: "Autor",
|
key: 'Autor',
|
||||||
type: "persona-nombre",
|
type: 'persona-nombre',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Autor",
|
label: 'Autor',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Asunto",
|
key: 'Asunto',
|
||||||
type: "raw",
|
type: 'raw',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Asunto",
|
label: 'Asunto',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"notas",
|
'notas',
|
||||||
document.querySelector("#cont"),
|
document.querySelector('#cont')
|
||||||
);
|
);
|
||||||
if (!checkRole("notas:edit")) {
|
if (!checkRole('notas:edit')) {
|
||||||
document.getElementById(btn_new).style.display = "none"
|
document.getElementById(btn_new).style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
document.getElementById(btn_new).onclick = () => {
|
document.getElementById(btn_new).onclick = () => {
|
||||||
setUrlHash("notas," + safeuuid(""));
|
setUrlHash('notas,' + safeuuid(''));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
427
src/page/panel.js
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
PERMS['panel'] = 'Panel';
|
||||||
|
PAGES.panel = {
|
||||||
|
navcss: 'btn2',
|
||||||
|
icon: 'static/appico/calendar.png',
|
||||||
|
AccessControl: true,
|
||||||
|
Title: 'Panel',
|
||||||
|
|
||||||
|
index: function () {
|
||||||
|
if (!checkRole('panel')) {
|
||||||
|
setUrlHash('index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentId = safeuuid();
|
||||||
|
container.innerHTML = html`
|
||||||
|
<h1>Panel de acogida del día</h1>
|
||||||
|
<p>Quiz de aprendizaje con retroalimentación para empezar la jornada.</p>
|
||||||
|
<div id="${contentId}">Cargando datos del día...</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
PAGES.panel
|
||||||
|
.__buildDailyContext()
|
||||||
|
.then((ctx) => {
|
||||||
|
var questions = PAGES.panel.__buildQuestions(ctx);
|
||||||
|
PAGES.panel.__renderQuiz(contentId, ctx, questions);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('Panel load error', e);
|
||||||
|
document.getElementById(contentId).innerHTML =
|
||||||
|
'<b>No se pudo cargar el Panel ahora mismo.</b>';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
__decryptIfNeeded: function (table, id, raw) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (typeof raw !== 'string') {
|
||||||
|
resolve(raw || {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TS_decrypt(
|
||||||
|
raw,
|
||||||
|
SECRET,
|
||||||
|
(data) => {
|
||||||
|
resolve(data || {});
|
||||||
|
},
|
||||||
|
table,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
__getTodayComedor: async function () {
|
||||||
|
var rows = await DB.list('comedor');
|
||||||
|
var today = CurrentISODate();
|
||||||
|
var items = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < rows.length; i++) {
|
||||||
|
var row = rows[i];
|
||||||
|
var data = await PAGES.panel.__decryptIfNeeded('comedor', row.id, row.data);
|
||||||
|
if ((data.Fecha || '') === today) {
|
||||||
|
items.push(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return {
|
||||||
|
Primero: '',
|
||||||
|
Segundo: '',
|
||||||
|
Postre: '',
|
||||||
|
Tipo: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
items.sort((a, b) => {
|
||||||
|
var ta = (a.Tipo || '').toLowerCase();
|
||||||
|
var tb = (b.Tipo || '').toLowerCase();
|
||||||
|
return ta < tb ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return items[0] || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
__getNotaById: async function (id) {
|
||||||
|
var data = await DB.get('notas', id);
|
||||||
|
if (!data) return {};
|
||||||
|
return await PAGES.panel.__decryptIfNeeded('notas', id, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
__getDiarioHoy: async function () {
|
||||||
|
var did = 'diario-' + CurrentISODate();
|
||||||
|
var data = await DB.get('aulas_informes', did);
|
||||||
|
if (!data) return {};
|
||||||
|
return await PAGES.panel.__decryptIfNeeded('aulas_informes', did, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
__extractFirstLine: function (text) {
|
||||||
|
var lines = String(text || '')
|
||||||
|
.split('\n')
|
||||||
|
.map((x) => x.trim())
|
||||||
|
.filter((x) => x !== '');
|
||||||
|
return lines[0] || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
__buildDailyContext: async function () {
|
||||||
|
var comedor = await PAGES.panel.__getTodayComedor();
|
||||||
|
var tareas = await PAGES.panel.__getNotaById('tareas');
|
||||||
|
var diario = await PAGES.panel.__getDiarioHoy();
|
||||||
|
|
||||||
|
var planHoy =
|
||||||
|
PAGES.panel.__extractFirstLine(tareas.Contenido) ||
|
||||||
|
PAGES.panel.__extractFirstLine(diario.Contenido) ||
|
||||||
|
'Revisar rutinas, colaborar y participar en las actividades del aula.';
|
||||||
|
|
||||||
|
return {
|
||||||
|
fecha: CurrentISODate(),
|
||||||
|
comedor: {
|
||||||
|
primero: (comedor.Primero || '').trim(),
|
||||||
|
primeroPicto: comedor.Primero_Picto || '',
|
||||||
|
segundo: (comedor.Segundo || '').trim(),
|
||||||
|
segundoPicto: comedor.Segundo_Picto || '',
|
||||||
|
postre: (comedor.Postre || '').trim(),
|
||||||
|
postrePicto: comedor.Postre_Picto || '',
|
||||||
|
tipo: (comedor.Tipo || '').trim(),
|
||||||
|
},
|
||||||
|
planHoy: planHoy,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
__hasPictoData: function (picto) {
|
||||||
|
return !!(
|
||||||
|
picto &&
|
||||||
|
typeof picto === 'object' &&
|
||||||
|
((picto.text || '').trim() !== '' || (picto.arasaacId || '').trim() !== '')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
__getOptionPicto: function (question, option) {
|
||||||
|
if (!question || !question.optionPictos) return '';
|
||||||
|
return question.optionPictos[String(option || '').trim()] || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
__renderOptionContent: function (question, option) {
|
||||||
|
var picto = PAGES.panel.__getOptionPicto(question, option);
|
||||||
|
var withPicto = typeof makePictoStatic === 'function' && PAGES.panel.__hasPictoData(picto);
|
||||||
|
if (!withPicto) return `<span>${option}</span>`;
|
||||||
|
return `
|
||||||
|
<span style="align-items:center; gap:8px;">
|
||||||
|
${makePictoStatic(picto)}
|
||||||
|
<span>${option}</span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
|
||||||
|
__pickDistractors: function (correct, pool, count) {
|
||||||
|
var options = [];
|
||||||
|
var seen = {};
|
||||||
|
var cleanCorrect = (correct || '').trim();
|
||||||
|
|
||||||
|
pool.forEach((item) => {
|
||||||
|
var text = String(item || '').trim();
|
||||||
|
if (text === '' || text === cleanCorrect || seen[text]) return;
|
||||||
|
seen[text] = true;
|
||||||
|
options.push(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
var out = [];
|
||||||
|
for (var i = 0; i < options.length && out.length < count; i++) {
|
||||||
|
out.push(options[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (out.length < count) {
|
||||||
|
out.push('No aplica hoy');
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
__shuffle: function (arr) {
|
||||||
|
var copy = arr.slice();
|
||||||
|
for (var i = copy.length - 1; i > 0; i--) {
|
||||||
|
var j = Math.floor(Math.random() * (i + 1));
|
||||||
|
var tmp = copy[i];
|
||||||
|
copy[i] = copy[j];
|
||||||
|
copy[j] = tmp;
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
},
|
||||||
|
|
||||||
|
__buildQuestions: function (ctx) {
|
||||||
|
var c = ctx.comedor || {};
|
||||||
|
var poolComedor = [c.primero, c.segundo, c.postre, 'No hay menú registrado'];
|
||||||
|
var comedorPictosByText = {};
|
||||||
|
if (c.primero) comedorPictosByText[c.primero] = c.primeroPicto || '';
|
||||||
|
if (c.segundo) comedorPictosByText[c.segundo] = c.segundoPicto || '';
|
||||||
|
if (c.postre) comedorPictosByText[c.postre] = c.postrePicto || '';
|
||||||
|
var questions = [];
|
||||||
|
|
||||||
|
if (c.primero) {
|
||||||
|
var opts1 = [c.primero].concat(PAGES.panel.__pickDistractors(c.primero, poolComedor, 3));
|
||||||
|
questions.push({
|
||||||
|
id: 'q-comida-primero',
|
||||||
|
text: '¿Qué hay de comer hoy de primero?',
|
||||||
|
options: PAGES.panel.__shuffle(opts1),
|
||||||
|
optionPictos: comedorPictosByText,
|
||||||
|
correct: c.primero,
|
||||||
|
ok: '¡Correcto! Ya sabes el primer plato de hoy.',
|
||||||
|
bad: 'Repasa el menú del día para anticipar la comida.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.segundo) {
|
||||||
|
var opts2 = [c.segundo].concat(PAGES.panel.__pickDistractors(c.segundo, poolComedor, 3));
|
||||||
|
questions.push({
|
||||||
|
id: 'q-comida-segundo',
|
||||||
|
text: '¿Y de segundo, qué toca?',
|
||||||
|
options: PAGES.panel.__shuffle(opts2),
|
||||||
|
optionPictos: comedorPictosByText,
|
||||||
|
correct: c.segundo,
|
||||||
|
ok: '¡Bien! Segundo identificado.',
|
||||||
|
bad: 'Casi. Mira el módulo Comedor para recordar el segundo plato.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.postre) {
|
||||||
|
var opts3 = [c.postre].concat(PAGES.panel.__pickDistractors(c.postre, poolComedor, 3));
|
||||||
|
questions.push({
|
||||||
|
id: 'q-comida-postre',
|
||||||
|
text: '¿Cuál es el postre de hoy?',
|
||||||
|
options: PAGES.panel.__shuffle(opts3),
|
||||||
|
optionPictos: comedorPictosByText,
|
||||||
|
correct: c.postre,
|
||||||
|
ok: '¡Perfecto! Postre acertado.',
|
||||||
|
bad: 'No pasa nada, revisa el postre en el menú diario.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var plan = ctx.planHoy || '';
|
||||||
|
var distractPlan = [
|
||||||
|
'No hay actividades planificadas hoy',
|
||||||
|
'Solo descanso todo el día',
|
||||||
|
'Actividad libre sin objetivos',
|
||||||
|
];
|
||||||
|
var planOptions = [plan].concat(PAGES.panel.__pickDistractors(plan, distractPlan, 3));
|
||||||
|
questions.push({
|
||||||
|
id: 'q-plan-hoy',
|
||||||
|
text: '¿Qué vamos a hacer hoy?',
|
||||||
|
options: PAGES.panel.__shuffle(planOptions),
|
||||||
|
correct: plan,
|
||||||
|
ok: '¡Muy bien! Tienes claro el plan del día.',
|
||||||
|
bad: 'Revisa las tareas/diario para conocer el plan del día.',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (questions.length === 0) {
|
||||||
|
questions.push({
|
||||||
|
id: 'q-fallback',
|
||||||
|
text: 'No hay menú cargado. ¿Qué acción es correcta ahora?',
|
||||||
|
options: [
|
||||||
|
'Consultar el módulo Comedor y las Notas del día',
|
||||||
|
'Ignorar la planificación diaria',
|
||||||
|
'Esperar sin revisar información',
|
||||||
|
'Saltar la acogida',
|
||||||
|
],
|
||||||
|
correct: 'Consultar el módulo Comedor y las Notas del día',
|
||||||
|
ok: 'Correcto. Ese es el siguiente paso recomendado.',
|
||||||
|
bad: 'La acogida mejora si revisamos menú y planificación diaria.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return questions;
|
||||||
|
},
|
||||||
|
|
||||||
|
__renderQuiz: function (contentId, ctx, questions) {
|
||||||
|
var target = document.getElementById(contentId);
|
||||||
|
var state = {
|
||||||
|
idx: 0,
|
||||||
|
answers: {},
|
||||||
|
score: 0,
|
||||||
|
feedback: '',
|
||||||
|
feedbackType: '',
|
||||||
|
reviewMode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function saveResult() {
|
||||||
|
var rid = CurrentISOTime() + '-' + safeuuid('');
|
||||||
|
var payload = {
|
||||||
|
Fecha: ctx.fecha,
|
||||||
|
Persona: SUB_LOGGED_IN_ID || '',
|
||||||
|
Aciertos: state.score,
|
||||||
|
Total: questions.length,
|
||||||
|
Respuestas: state.answers,
|
||||||
|
};
|
||||||
|
DB.put('panel_respuestas', rid, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCurrent() {
|
||||||
|
var q = questions[state.idx];
|
||||||
|
if (!q) return;
|
||||||
|
|
||||||
|
var selected = state.answers[q.id] || '';
|
||||||
|
var optionsHtml = q.options
|
||||||
|
.map((option, i) => {
|
||||||
|
var oid = safeuuid();
|
||||||
|
var checked = selected === option ? 'checked' : '';
|
||||||
|
var disabled = state.reviewMode ? 'disabled' : '';
|
||||||
|
var optionStyle =
|
||||||
|
'display:block;margin: 8px 0;padding: 8px;border: 1px solid #ccc;border-radius: 6px;cursor:pointer;max-width: 150px;text-align: center;';
|
||||||
|
|
||||||
|
if (state.reviewMode) {
|
||||||
|
if (option === q.correct) {
|
||||||
|
optionStyle =
|
||||||
|
'display:block;margin: 8px 0;padding: 8px;border: 2px solid #2ed573;background:#eafff1;border-radius: 6px;cursor:pointer;max-width: 150px;text-align: center;';
|
||||||
|
} else if (option === selected && option !== q.correct) {
|
||||||
|
optionStyle =
|
||||||
|
'display:block;margin: 8px 0;padding: 8px;border: 2px solid #ff4757;background:#ffecec;border-radius: 6px;cursor:pointer;max-width: 150px;text-align: center;';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var optionContent = PAGES.panel.__renderOptionContent(q, option);
|
||||||
|
return `
|
||||||
|
<label class="panel-option" for="${oid}" style="${optionStyle}">
|
||||||
|
<input id="${oid}" type="radio" name="panel-question" value="${option.replace(/"/g, '"')}" ${checked} ${disabled} />
|
||||||
|
${optionContent}
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
var feedbackColor = '#555';
|
||||||
|
if (state.feedbackType === 'ok') feedbackColor = '#1f8f4a';
|
||||||
|
if (state.feedbackType === 'bad') feedbackColor = '#c0392b';
|
||||||
|
|
||||||
|
var nextButtonText = state.reviewMode ? 'Continuar' : 'Comprobar';
|
||||||
|
|
||||||
|
target.innerHTML = html`
|
||||||
|
<fieldset style="max-width: 800px;">
|
||||||
|
<legend>Pregunta ${state.idx + 1} de ${questions.length}</legend>
|
||||||
|
<div style="margin-bottom: 10px;"><b>${q.text}</b></div>
|
||||||
|
<small>
|
||||||
|
Menú hoy: ${ctx.comedor.primero || '—'} / ${ctx.comedor.segundo || '—'} /
|
||||||
|
${ctx.comedor.postre || '—'}
|
||||||
|
</small>
|
||||||
|
<div style="margin-top: 10px; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px;">${optionsHtml}</div>
|
||||||
|
<div id="panel-feedback" style="margin-top: 12px; color:${feedbackColor};"><i>${state.feedback || ''}</i></div>
|
||||||
|
<div style="margin-top: 12px; display:flex; gap:8px;">
|
||||||
|
<button class="btn5" id="panel-next">${nextButtonText}</button>
|
||||||
|
<button id="panel-cancel">Salir</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('panel-cancel').onclick = () => setUrlHash('index');
|
||||||
|
document.getElementById('panel-next').onclick = () => {
|
||||||
|
if (state.reviewMode) {
|
||||||
|
state.reviewMode = false;
|
||||||
|
state.feedback = '';
|
||||||
|
state.feedbackType = '';
|
||||||
|
|
||||||
|
if (state.idx < questions.length - 1) {
|
||||||
|
state.idx++;
|
||||||
|
renderCurrent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveResult();
|
||||||
|
renderFinal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var checked = document.querySelector('input[name="panel-question"]:checked');
|
||||||
|
if (!checked) {
|
||||||
|
state.feedback = 'Selecciona una opción antes de continuar.';
|
||||||
|
state.feedbackType = 'bad';
|
||||||
|
renderCurrent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var answer = checked.value;
|
||||||
|
state.answers[q.id] = answer;
|
||||||
|
|
||||||
|
var wasCorrect = answer === q.correct;
|
||||||
|
if (wasCorrect) {
|
||||||
|
state.score++;
|
||||||
|
state.feedback = '✅ ' + q.ok;
|
||||||
|
state.feedbackType = 'ok';
|
||||||
|
} else {
|
||||||
|
state.feedback = '❌ ' + q.bad + ' Respuesta esperada: ' + q.correct;
|
||||||
|
state.feedbackType = 'bad';
|
||||||
|
}
|
||||||
|
|
||||||
|
state.reviewMode = true;
|
||||||
|
renderCurrent();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFinal() {
|
||||||
|
var total = questions.length;
|
||||||
|
var ratio = total > 0 ? Math.round((state.score / total) * 100) : 0;
|
||||||
|
var msg = 'Buen trabajo. Sigue reforzando la acogida diaria.';
|
||||||
|
if (ratio >= 80) msg = 'Excelente acogida: gran comprensión del día.';
|
||||||
|
else if (ratio >= 50) msg = 'Buen avance. Revisa comedor/tareas para reforzar.';
|
||||||
|
|
||||||
|
target.innerHTML = html`
|
||||||
|
<fieldset style="max-width: 800px;">
|
||||||
|
<legend>Resultado del Panel</legend>
|
||||||
|
<h2>${state.score} / ${total} aciertos (${ratio}%)</h2>
|
||||||
|
<p>${msg}</p>
|
||||||
|
<p><b>Plan de hoy:</b> ${ctx.planHoy}</p>
|
||||||
|
<button class="btn5" id="panel-repeat">Repetir quiz</button>
|
||||||
|
<button id="panel-home">Volver al inicio</button>
|
||||||
|
</fieldset>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('panel-repeat').onclick = () => {
|
||||||
|
state.idx = 0;
|
||||||
|
state.answers = {};
|
||||||
|
state.score = 0;
|
||||||
|
state.feedback = '';
|
||||||
|
state.feedbackType = '';
|
||||||
|
state.reviewMode = false;
|
||||||
|
renderCurrent();
|
||||||
|
};
|
||||||
|
document.getElementById('panel-home').onclick = () => setUrlHash('index');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCurrent();
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
PERMS["personas"] = "Personas";
|
PERMS['personas'] = 'Personas';
|
||||||
PERMS["personas:edit"] = "> Editar";
|
PERMS['personas:edit'] = '> Editar';
|
||||||
PAGES.personas = {
|
PAGES.personas = {
|
||||||
navcss: "btn3",
|
navcss: 'btn3',
|
||||||
icon: "static/appico/users.png",
|
icon: 'static/appico/users.png',
|
||||||
AccessControl: true,
|
AccessControl: true,
|
||||||
Title: "Personas",
|
Title: 'Personas',
|
||||||
edit: function (mid) {
|
edit: function (mid) {
|
||||||
if (!checkRole("personas:edit")) {
|
if (!checkRole('personas:edit')) {
|
||||||
setUrlHash("personas");
|
setUrlHash('personas');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var nameh1 = safeuuid();
|
var nameh1 = safeuuid();
|
||||||
@@ -17,87 +17,93 @@ PAGES.personas = {
|
|||||||
var field_notas = safeuuid();
|
var field_notas = safeuuid();
|
||||||
var field_anilla = safeuuid();
|
var field_anilla = safeuuid();
|
||||||
var field_foto = safeuuid();
|
var field_foto = safeuuid();
|
||||||
|
var field_oculto = safeuuid();
|
||||||
var render_foto = safeuuid();
|
var render_foto = safeuuid();
|
||||||
var field_monedero_balance = safeuuid();
|
var field_monedero_balance = safeuuid();
|
||||||
var field_monedero_notas = safeuuid();
|
|
||||||
var btn_guardar = safeuuid();
|
var btn_guardar = safeuuid();
|
||||||
var btn_borrar = safeuuid();
|
var btn_borrar = safeuuid();
|
||||||
var btn_ver_monedero = safeuuid();
|
var btn_ver_monedero = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Persona <code id="${nameh1}"></code></h1>
|
<h1>Persona <code id="${nameh1}"></code></h1>
|
||||||
${BuildQR("personas," + mid, "Esta Persona")}
|
<fieldset style="width: 100%;max-width: 980px;box-sizing: border-box;">
|
||||||
<fieldset>
|
<div style="display: flex;flex-wrap: wrap;gap: 10px 16px;">
|
||||||
<label>
|
<label style="display: flex;flex-direction: column;gap: 6px;max-width: 105px;flex: 1 1 105px;">
|
||||||
Nombre<br>
|
Foto
|
||||||
<input type="text" id="${field_nombre}"><br><br>
|
<img id="${render_foto}" height="100px" style="border: 3px inset; min-width: 7px; width: fit-content;" src="static/ico/user_generic.png">
|
||||||
|
<input type="file" accept="image/*" id="${field_foto}" style="display: none;">
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label style="display: flex;flex-direction: column;gap: 6px;min-width: 220px;flex: 1 1 280px;">
|
||||||
Zona<br>
|
Nombre
|
||||||
<input type="text" id="${field_zona}"><br><br>
|
<input type="text" id="${field_nombre}">
|
||||||
</label>
|
</label>
|
||||||
|
<label style="display: flex;flex-direction: column;gap: 6px;min-width: 170px;flex: 1 1 170px;">
|
||||||
|
Zona
|
||||||
|
<input type="text" id="${field_zona}">
|
||||||
</label>
|
</label>
|
||||||
<details>
|
<label style="display: flex;flex-direction: column;gap: 6px;max-width: 170px;flex: 1 1 170px;">
|
||||||
|
Saldo Monedero
|
||||||
|
<input type="number" step="0.01" id="${field_monedero_balance}" disabled style="color: #000; font-weight: bold;">
|
||||||
|
</label>
|
||||||
|
<label style="display: flex;flex-direction: column;gap: 6px;max-width: 50px;flex: 1 1 50px;">
|
||||||
|
Anilla
|
||||||
|
<input type="color" id="${field_anilla}">
|
||||||
|
</label>
|
||||||
|
<label style="display: flex;flex-direction: column;gap: 6px;max-width: 60px;flex: 1 1 60px;">
|
||||||
|
Ocultar?
|
||||||
|
<input type="checkbox" id="${field_oculto}" style="height: 50px; width: 50px; margin: 0;">
|
||||||
|
</label>
|
||||||
|
<label style="display: flex;flex-direction: column;gap: 6px;min-width: 220px;flex: 1 1 100%;">
|
||||||
|
Notas
|
||||||
|
<textarea id="${field_notas}"></textarea>
|
||||||
|
</label>
|
||||||
|
<details style="flex: 1 1 100%;">
|
||||||
<summary>Permisos</summary>
|
<summary>Permisos</summary>
|
||||||
<form id="${permisosdet}">
|
<form id="${permisosdet}">
|
||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
<label>
|
<details style="background: #e3fde3ff; border: 2px solid #21f328ff; border-radius: 8px; padding: 10px; margin: 15px 0; display: none; flex: 1 1 100%;">
|
||||||
Anilla<br>
|
|
||||||
<input type="color" id="${field_anilla}"><br><br>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Foto (PNG o JPG)<br>
|
|
||||||
<img id="${render_foto}" height="100px" style="border: 3px inset; min-width: 7px;" src="static/ico/user_generic.png">
|
|
||||||
<input type="file" accept="image/*" id="${field_foto}" style="display: none;"><br><br>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<details style="background: #e3f2fd; border: 2px solid #2196f3; border-radius: 8px; padding: 10px; margin: 15px 0;">
|
|
||||||
<summary style="cursor: pointer; font-weight: bold; color: #1976d2;">💳 Tarjeta Monedero</summary>
|
|
||||||
<div style="padding: 15px;">
|
|
||||||
<label>
|
|
||||||
Balance Actual<br>
|
|
||||||
<input type="number" step="0.01" id="${field_monedero_balance}" style="font-size: 24px; font-weight: bold; color: #1976d2;"><br>
|
|
||||||
<small>Se actualiza automáticamente con las transacciones</small><br><br>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Notas del Monedero<br>
|
|
||||||
<textarea id="${field_monedero_notas}" rows="3" placeholder="Notas adicionales sobre el monedero..."></textarea><br><br>
|
|
||||||
</label>
|
|
||||||
<button type="button" id="${btn_ver_monedero}" class="btn5">Ver Transacciones del Monedero</button>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<details style="background: #e3fde3ff; border: 2px solid #21f328ff; border-radius: 8px; padding: 10px; margin: 15px 0; display: none;">
|
|
||||||
<summary style="cursor: pointer; font-weight: bold; color: rgba(26, 141, 3, 1);">🔗 Generar enlaces</summary>
|
<summary style="cursor: pointer; font-weight: bold; color: rgba(26, 141, 3, 1);">🔗 Generar enlaces</summary>
|
||||||
<div style="padding: 15px;">
|
<div style="padding: 15px;display: flex;flex-wrap: wrap;gap: 10px 16px;align-items: flex-end;">
|
||||||
<label>
|
<label style="display: flex;flex-direction: column;gap: 6px;min-width: 220px;flex: 1 1 100%;">
|
||||||
Este servidor<br>
|
Este servidor
|
||||||
<input type="url" value="${location.protocol}//${location.hostname}:${location.port}${location.pathname}?login=${getDBName()}:${SECRET}&sublogin=${mid}" style="font-size: 10px; font-weight: bold; color: #000;"><br>
|
<input type="url" value="${location.protocol}//${location.hostname}:${location.port}${location.pathname}?sublogin=${mid}" style="font-size: 10px; font-weight: bold; color: #000;">
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Cualquier Servidor<br>
|
|
||||||
<input type="url" value="https://tech.eus/ts/?login=${getDBName()}:${SECRET}&sublogin=${mid}" style="font-size: 10px; font-weight: bold; color: #000;"><br>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
</div>
|
||||||
<label>
|
<hr>
|
||||||
Notas<br>
|
<button class="saveico" id="${btn_guardar}">
|
||||||
<textarea id="${field_notas}"></textarea><br><br>
|
<img src="static/floppy_disk_green.png" />
|
||||||
</label><hr>
|
<br>Guardar
|
||||||
<button class="btn5" id="${btn_guardar}">Guardar</button>
|
</button>
|
||||||
<button class="rojo" id="${btn_borrar}">Borrar</button>
|
<button class="delico" id="${btn_borrar}">
|
||||||
|
<img src="static/garbage.png" />
|
||||||
|
<br>Borrar
|
||||||
|
</button>
|
||||||
|
<button type="button" id="${btn_ver_monedero}" class="opicon">
|
||||||
|
<img src="static/cash_flow.png" />
|
||||||
|
<br>Movimientos
|
||||||
|
</button>
|
||||||
|
<button class="opicon" onclick="setUrlHash('personas')" style="float: right;"> <!-- Align to the right -->
|
||||||
|
<img src="static/exit.png" />
|
||||||
|
<br>Salir
|
||||||
|
</button>
|
||||||
|
<button class="opicon" onclick="window.print()" style="float: right;"> <!-- Align to the right -->
|
||||||
|
<img src="static/printer2.png" />
|
||||||
|
<br>Imprimir
|
||||||
|
</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
var resized = "";
|
var resized = '';
|
||||||
var pdel = document.getElementById(permisosdet);
|
var pdel = document.getElementById(permisosdet);
|
||||||
DB.get('personas', mid).then((data) => {
|
DB.get('personas', mid).then((data) => {
|
||||||
function load_data(data, ENC = "") {
|
function load_data(data, ENC = '') {
|
||||||
document.getElementById(nameh1).innerText = mid;
|
document.getElementById(nameh1).innerText = mid;
|
||||||
var pot = "<ul>";
|
var pot = '<ul>';
|
||||||
Object.entries(PERMS).forEach((page) => {
|
Object.entries(PERMS).forEach((page) => {
|
||||||
var c = "";
|
var c = '';
|
||||||
if ((data["Roles"] || ",").split(",").includes(page[0])) {
|
if ((data['Roles'] || ',').split(',').includes(page[0])) {
|
||||||
c = "checked";
|
c = 'checked';
|
||||||
}
|
}
|
||||||
pot += `
|
pot += `
|
||||||
<li><label>
|
<li><label>
|
||||||
@@ -106,35 +112,41 @@ PAGES.personas = {
|
|||||||
</label></li>
|
</label></li>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
pdel.innerHTML = pot + "</ul>";
|
pdel.innerHTML = pot + '</ul>';
|
||||||
document.getElementById(field_nombre).value = data["Nombre"] || "";
|
document.getElementById(field_nombre).value = data['Nombre'] || '';
|
||||||
document.getElementById(field_zona).value = data["Region"] || "";
|
document.getElementById(field_zona).value = data['Region'] || '';
|
||||||
document.getElementById(field_anilla).value = data["SC_Anilla"] || "";
|
document.getElementById(field_anilla).value = data['SC_Anilla'] || '';
|
||||||
|
document.getElementById(field_oculto).checked = data['Oculto'] || false;
|
||||||
// set fallback image immediately
|
// set fallback image immediately
|
||||||
document.getElementById(render_foto).src = 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";
|
resized = data['Foto'] || 'static/ico/user_generic.png';
|
||||||
// try to load attachment 'foto' if present (preferred storage)
|
// try to load attachment 'foto' if present (preferred storage)
|
||||||
DB.getAttachment('personas', mid, 'foto').then((durl) => {
|
DB.getAttachment('personas', mid, 'foto')
|
||||||
|
.then((durl) => {
|
||||||
if (durl) {
|
if (durl) {
|
||||||
document.getElementById(render_foto).src = durl;
|
document.getElementById(render_foto).src = durl;
|
||||||
resized = durl;
|
resized = durl;
|
||||||
}
|
}
|
||||||
}).catch(() => {});
|
})
|
||||||
document.getElementById(field_notas).value = data["markdown"] || "";
|
.catch(() => {});
|
||||||
document.getElementById(field_monedero_balance).value =
|
document.getElementById(field_notas).value = data['markdown'] || '';
|
||||||
data["Monedero_Balance"] || 0;
|
document.getElementById(field_monedero_balance).value = data['Monedero_Balance'] || 0;
|
||||||
document.getElementById(field_monedero_notas).value =
|
|
||||||
data["Monedero_Notas"] || "";
|
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
TS_decrypt(
|
||||||
load_data(data, "%E");
|
data,
|
||||||
}, 'personas', mid);
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
|
load_data(data, '%E');
|
||||||
|
},
|
||||||
|
'personas',
|
||||||
|
mid
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
load_data(data || {});
|
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];
|
const file = e.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
// Do NOT resize — keep original uploaded image
|
// Do NOT resize — keep original uploaded image
|
||||||
@@ -152,70 +164,72 @@ PAGES.personas = {
|
|||||||
if (guardarBtn.disabled) return;
|
if (guardarBtn.disabled) return;
|
||||||
|
|
||||||
guardarBtn.disabled = true;
|
guardarBtn.disabled = true;
|
||||||
guardarBtn.style.opacity = "0.5";
|
guardarBtn.style.opacity = '0.5';
|
||||||
|
|
||||||
var dt = new FormData(pdel);
|
var dt = new FormData(pdel);
|
||||||
var data = {
|
var data = {
|
||||||
Nombre: document.getElementById(field_nombre).value,
|
Nombre: document.getElementById(field_nombre).value,
|
||||||
Region: document.getElementById(field_zona).value,
|
Region: document.getElementById(field_zona).value,
|
||||||
Roles: dt.getAll("perm").join(",") + ",",
|
Roles: dt.getAll('perm').join(',') + ',',
|
||||||
SC_Anilla: document.getElementById(field_anilla).value,
|
SC_Anilla: document.getElementById(field_anilla).value,
|
||||||
|
Oculto: document.getElementById(field_oculto).checked,
|
||||||
// Foto moved to PouchDB attachment named 'foto'
|
// Foto moved to PouchDB attachment named 'foto'
|
||||||
markdown: document.getElementById(field_notas).value,
|
markdown: document.getElementById(field_notas).value,
|
||||||
Monedero_Balance:
|
Monedero_Balance: parseFloat(document.getElementById(field_monedero_balance).value) || 0,
|
||||||
parseFloat(document.getElementById(field_monedero_balance).value) ||
|
|
||||||
0,
|
|
||||||
Monedero_Notas: document.getElementById(field_monedero_notas).value,
|
|
||||||
};
|
};
|
||||||
document.getElementById("actionStatus").style.display = "block";
|
document.getElementById('actionStatus').style.display = 'block';
|
||||||
DB.put('personas', mid, data).then(() => {
|
DB.put('personas', mid, data)
|
||||||
|
.then(() => {
|
||||||
// if resized is a data URL (new/updated image), save as attachment
|
// if resized is a data URL (new/updated image), save as attachment
|
||||||
var attachPromise = Promise.resolve(true);
|
var attachPromise = Promise.resolve(true);
|
||||||
if (typeof resized === 'string' && resized.indexOf('data:') === 0) {
|
if (typeof resized === 'string' && resized.indexOf('data:') === 0) {
|
||||||
attachPromise = DB.putAttachment('personas', mid, 'foto', resized, 'image/png');
|
attachPromise = DB.putAttachment('personas', mid, 'foto', resized, 'image/png');
|
||||||
}
|
}
|
||||||
attachPromise.then(() => {
|
attachPromise
|
||||||
toastr.success("Guardado!");
|
.then(() => {
|
||||||
|
toastr.success('Guardado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
setUrlHash("personas");
|
setUrlHash('personas');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
console.warn('putAttachment error', e);
|
console.warn('putAttachment error', e);
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
guardarBtn.disabled = false;
|
guardarBtn.disabled = false;
|
||||||
guardarBtn.style.opacity = "1";
|
guardarBtn.style.opacity = '1';
|
||||||
toastr.error("Error al guardar la foto");
|
toastr.error('Error al guardar la foto');
|
||||||
});
|
});
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
console.warn('DB.put error', e);
|
console.warn('DB.put error', e);
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
guardarBtn.disabled = false;
|
guardarBtn.disabled = false;
|
||||||
guardarBtn.style.opacity = "1";
|
guardarBtn.style.opacity = '1';
|
||||||
toastr.error("Error al guardar la persona");
|
toastr.error('Error al guardar la persona');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
document.getElementById(btn_ver_monedero).onclick = () => {
|
document.getElementById(btn_ver_monedero).onclick = () => {
|
||||||
setUrlHash("pagos"); // Navigate to pagos and show transactions for this person
|
setUrlHash('pagos?filter=Persona:' + encodeURIComponent(mid)); // Navigate to pagos and show transactions for this person
|
||||||
};
|
};
|
||||||
document.getElementById(btn_borrar).onclick = () => {
|
document.getElementById(btn_borrar).onclick = () => {
|
||||||
if (confirm("¿Quieres borrar esta persona?") == true) {
|
if (confirm('¿Quieres borrar esta persona?') == true) {
|
||||||
DB.del('personas', mid).then(() => {
|
DB.del('personas', mid).then(() => {
|
||||||
toastr.error("Borrado!");
|
toastr.error('Borrado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setUrlHash("personas");
|
setUrlHash('personas');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
index: function () {
|
index: function () {
|
||||||
if (!checkRole("personas")) {
|
if (!checkRole('personas')) {
|
||||||
setUrlHash("index");
|
setUrlHash('index');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var btn_new = safeuuid();
|
var btn_new = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Personas</h1>
|
<h1>Personas</h1>
|
||||||
<button id="${btn_new}">Nueva Persona</button>
|
<button id="${btn_new}">Nueva Persona</button>
|
||||||
<div id="tableContainer"></div>
|
<div id="tableContainer"></div>
|
||||||
@@ -227,28 +241,28 @@ PAGES.personas = {
|
|||||||
// type: "persona",
|
// type: "persona",
|
||||||
// self: true,
|
// self: true,
|
||||||
// },
|
// },
|
||||||
{ key: "Foto", label: "Foto", type: "attachment-persona", default: "", self: true },
|
{ key: 'Foto', label: 'Foto', type: 'attachment-persona', default: '', self: true },
|
||||||
{ key: "Nombre", label: "Nombre", type: "text", default: "" },
|
{ key: 'Nombre', label: 'Nombre', type: 'text', default: '' },
|
||||||
{ key: "Region", label: "Zona", type: "text", default: "" },
|
{ key: 'Region', label: 'Zona', type: 'text', default: '' },
|
||||||
{ key: "Monedero_Balance", label: "Saldo Monedero", type: "moneda" },
|
{ key: 'Monedero_Balance', label: 'Saldo Monedero', type: 'moneda' },
|
||||||
//{ key: "markdown", label: "Notas", type: "markdown", default: "" },
|
//{ key: "markdown", label: "Notas", type: "markdown", default: "" },
|
||||||
//{ key: "Roles", label: "Permisos", type: "text", default: "" }
|
//{ key: "Roles", label: "Permisos", type: "text", default: "" }
|
||||||
];
|
];
|
||||||
|
|
||||||
TS_IndexElement(
|
TS_IndexElement(
|
||||||
"personas",
|
'personas',
|
||||||
config,
|
config,
|
||||||
"personas",
|
'personas',
|
||||||
document.getElementById("tableContainer"),
|
document.getElementById('tableContainer'),
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
true // Enable global search bar
|
true // Enable global search bar
|
||||||
);
|
);
|
||||||
if (!checkRole("personas:edit")) {
|
if (!checkRole('personas:edit')) {
|
||||||
document.getElementById(btn_new).style.display = "none";
|
document.getElementById(btn_new).style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
document.getElementById(btn_new).onclick = () => {
|
document.getElementById(btn_new).onclick = () => {
|
||||||
setUrlHash("personas," + safeuuid(""));
|
setUrlHash('personas,' + safeuuid(''));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,25 +1,50 @@
|
|||||||
PERMS["resumen_diario"] = "Resumen diario (Solo docentes!)";
|
PERMS['resumen_diario'] = 'Resumen diario (Solo docentes!)';
|
||||||
PAGES.resumen_diario = {
|
PAGES.resumen_diario = {
|
||||||
icon: "static/appico/calendar.png",
|
icon: 'static/appico/calendar.png',
|
||||||
navcss: "btn3",
|
navcss: 'btn3',
|
||||||
AccessControl: true,
|
AccessControl: true,
|
||||||
Title: "Resumen Diario",
|
Title: 'Resumen Diario',
|
||||||
index: function () {
|
index: function () {
|
||||||
var data_Comedor = safeuuid();
|
var data_Comedor = safeuuid();
|
||||||
var data_Tareas = safeuuid();
|
var data_Tareas = safeuuid();
|
||||||
var data_Diario = safeuuid();
|
var data_Diario = safeuuid();
|
||||||
var data_Weather = safeuuid();
|
var data_Weather = safeuuid();
|
||||||
if (!checkRole("resumen_diario")) {
|
if (!checkRole('resumen_diario')) {
|
||||||
setUrlHash("index");
|
setUrlHash('index');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Resumen Diario ${CurrentISODate()}</h1>
|
<h1>Resumen Diario ${CurrentISODate()}</h1>
|
||||||
<button onclick="print()">Imprimir</button>
|
<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
|
||||||
<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>
|
class="btn7"
|
||||||
<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>
|
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"
|
||||||
<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>
|
><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
|
//#region Cargar Clima
|
||||||
@@ -27,15 +52,16 @@ PAGES.resumen_diario = {
|
|||||||
// url format: https://wttr.in/<loc>?F0m
|
// url format: https://wttr.in/<loc>?F0m
|
||||||
DB.get('settings', 'weather_location').then((loc) => {
|
DB.get('settings', 'weather_location').then((loc) => {
|
||||||
if (!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) {
|
if (loc) {
|
||||||
DB.put('settings', 'weather_location', loc);
|
DB.put('settings', 'weather_location', loc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (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 {
|
} 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
|
//#endregion Cargar Clima
|
||||||
@@ -43,17 +69,20 @@ PAGES.resumen_diario = {
|
|||||||
DB.get('comedor', CurrentISODate()).then((data) => {
|
DB.get('comedor', CurrentISODate()).then((data) => {
|
||||||
function add_row(data) {
|
function add_row(data) {
|
||||||
// Fix newlines
|
// Fix newlines
|
||||||
data.Platos = data.Platos || "No hay platos registrados para hoy.";
|
data.Platos = data.Platos || 'No hay platos registrados para hoy.';
|
||||||
// Display platos
|
// Display platos
|
||||||
document.getElementById(data_Comedor).innerHTML = data.Platos.replace(
|
document.getElementById(data_Comedor).innerHTML = data.Platos.replace(/\n/g, '<br>');
|
||||||
/\n/g,
|
|
||||||
"<br>"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
TS_decrypt(
|
||||||
|
data,
|
||||||
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
add_row(data || {});
|
add_row(data || {});
|
||||||
}, 'comedor', CurrentISODate());
|
},
|
||||||
|
'comedor',
|
||||||
|
CurrentISODate()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
add_row(data || {});
|
add_row(data || {});
|
||||||
}
|
}
|
||||||
@@ -63,17 +92,20 @@ PAGES.resumen_diario = {
|
|||||||
DB.get('notas', 'tareas').then((data) => {
|
DB.get('notas', 'tareas').then((data) => {
|
||||||
function add_row(data) {
|
function add_row(data) {
|
||||||
// Fix newlines
|
// Fix newlines
|
||||||
data.Contenido = data.Contenido || "No hay tareas.";
|
data.Contenido = data.Contenido || 'No hay tareas.';
|
||||||
// Display platos
|
// Display platos
|
||||||
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(
|
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(/\n/g, '<br>');
|
||||||
/\n/g,
|
|
||||||
"<br>"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
TS_decrypt(
|
||||||
|
data,
|
||||||
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
add_row(data || {});
|
add_row(data || {});
|
||||||
}, 'notas', 'tareas');
|
},
|
||||||
|
'notas',
|
||||||
|
'tareas'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
add_row(data || {});
|
add_row(data || {});
|
||||||
}
|
}
|
||||||
@@ -83,17 +115,20 @@ PAGES.resumen_diario = {
|
|||||||
DB.get('aulas_informes', 'diario-' + CurrentISODate()).then((data) => {
|
DB.get('aulas_informes', 'diario-' + CurrentISODate()).then((data) => {
|
||||||
function add_row(data) {
|
function add_row(data) {
|
||||||
// Fix newlines
|
// Fix newlines
|
||||||
data.Contenido = data.Contenido || "No hay un diario.";
|
data.Contenido = data.Contenido || 'No hay un diario.';
|
||||||
// Display platos
|
// Display platos
|
||||||
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(
|
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(/\n/g, '<br>');
|
||||||
/\n/g,
|
|
||||||
"<br>"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
TS_decrypt(
|
||||||
|
data,
|
||||||
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
add_row(data || {});
|
add_row(data || {});
|
||||||
}, 'aulas_informes', 'diario-' + CurrentISODate());
|
},
|
||||||
|
'aulas_informes',
|
||||||
|
'diario-' + CurrentISODate()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
add_row(data || {});
|
add_row(data || {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
PERMS["supercafe"] = "Cafetería";
|
PERMS['supercafe'] = 'Cafetería';
|
||||||
PERMS["supercafe:edit"] = "> Editar";
|
PERMS['supercafe:edit'] = '> Editar';
|
||||||
PAGES.supercafe = {
|
PAGES.supercafe = {
|
||||||
navcss: "btn4",
|
navcss: 'btn4',
|
||||||
icon: "static/appico/cup.png",
|
icon: 'static/appico/cup.png',
|
||||||
AccessControl: true,
|
AccessControl: true,
|
||||||
Title: "Cafetería",
|
Title: 'Cafetería',
|
||||||
edit: function (mid) {
|
edit: function (mid) {
|
||||||
if (!checkRole("supercafe:edit")) {
|
if (!checkRole('supercafe:edit')) {
|
||||||
setUrlHash("supercafe");
|
setUrlHash('supercafe');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var nameh1 = safeuuid();
|
var nameh1 = safeuuid();
|
||||||
@@ -19,48 +19,78 @@ PAGES.supercafe = {
|
|||||||
var div_actions = safeuuid();
|
var div_actions = safeuuid();
|
||||||
var btn_guardar = safeuuid();
|
var btn_guardar = safeuuid();
|
||||||
var btn_borrar = safeuuid();
|
var btn_borrar = safeuuid();
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Comanda <code id="${nameh1}"></code></h1>
|
<h1>Comanda <code id="${nameh1}"></code></h1>
|
||||||
<button onclick="setUrlHash('supercafe');">Salir</button>
|
<button onclick="setUrlHash('supercafe');">Salir</button>
|
||||||
<fieldset style="text-align: center;">
|
<fieldset style="text-align: center;">
|
||||||
<legend>Rellenar comanda</legend>
|
<legend>Rellenar comanda</legend>
|
||||||
<label style="display: none;">
|
<label style="display: none;">
|
||||||
Fecha<br>
|
Fecha<br />
|
||||||
<input readonly disabled type="text" id="${field_fecha}" value=""><br><br>
|
<input readonly disabled type="text" id="${field_fecha}" value="" /><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<label style="display: none;">
|
<label style="display: none;">
|
||||||
Persona<br>
|
Persona<br />
|
||||||
<input type="hidden" id="${field_persona}">
|
<input type="hidden" id="${field_persona}" />
|
||||||
<br><br>
|
<br /><br />
|
||||||
</label>
|
</label>
|
||||||
<label style="display: none;">
|
<label style="display: none;">
|
||||||
Comanda (utiliza el panel de relleno)<br>
|
Comanda (utiliza el panel de relleno)<br />
|
||||||
<textarea readonly disabled id="${field_comanda}"></textarea><br><br>
|
<textarea readonly disabled id="${field_comanda}"></textarea><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<div id="${div_actions}" open>
|
<div id="${div_actions}" open>
|
||||||
<!--<summary>Mostrar botones de relleno</summary>-->
|
<!--<summary>Mostrar botones de relleno</summary>-->
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
Notas<br>
|
Notas<br />
|
||||||
<textarea id="${field_notas}"></textarea><br><br>
|
<textarea id="${field_notas}"></textarea><br /><br />
|
||||||
</label>
|
</label>
|
||||||
<label style="display: none;">
|
<label style="display: none;">
|
||||||
Estado<br>
|
Estado<br />
|
||||||
<input readonly disabled type="text" id="${field_estado}" value="%%">
|
<input readonly disabled type="text" id="${field_estado}" value="%%" />
|
||||||
<br>Modificar en el listado de comandas<br>
|
<br />Modificar en el listado de comandas<br />
|
||||||
</label>
|
</label>
|
||||||
<button id=${btn_guardar} class="btn5">Guardar</button>
|
<button id=${btn_guardar} class="btn5">Guardar</button>
|
||||||
<button id=${btn_borrar} class="rojo">Borrar</button>
|
<button id=${btn_borrar} class="rojo">Borrar</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
var currentData = {};
|
var currentData = {};
|
||||||
var currentPersonaID = "";
|
var currentPersonaID = '';
|
||||||
var divact = document.getElementById(div_actions);
|
var divact = document.getElementById(div_actions);
|
||||||
|
|
||||||
function loadActions() {
|
function loadActions() {
|
||||||
divact.innerHTML = "";
|
divact.innerHTML = '';
|
||||||
addCategory_Personas(divact, SC_Personas, currentPersonaID, (value) => {
|
addCategory_Personas(divact, SC_Personas, currentPersonaID, (value) => {
|
||||||
document.getElementById(field_persona).value = value;
|
document.getElementById(field_persona).value = value;
|
||||||
|
|
||||||
|
// Check for outstanding debts when person is selected
|
||||||
|
DB.list('supercafe').then((rows) => {
|
||||||
|
var deudasCount = 0;
|
||||||
|
var processed = 0;
|
||||||
|
var total = rows.length;
|
||||||
|
|
||||||
|
if (total === 0) return;
|
||||||
|
|
||||||
|
// Count debts for this person
|
||||||
|
rows.forEach((row) => {
|
||||||
|
TS_decrypt(row.data, SECRET, (data) => {
|
||||||
|
if (data.Persona == value && data.Estado == 'Deuda') {
|
||||||
|
deudasCount++;
|
||||||
|
}
|
||||||
|
processed++;
|
||||||
|
|
||||||
|
// When all rows are processed, show warning if needed
|
||||||
|
if (processed === total && deudasCount >= 3) {
|
||||||
|
var tts_msg = `Atención: Esta persona tiene ${deudasCount} comandas en deuda. No se podrá guardar el pedido.`;
|
||||||
|
TS_SayTTS(tts_msg)
|
||||||
|
toastr.warning(`Esta persona tiene ${deudasCount} comandas en deuda. No se podrá guardar el pedido.`, '', {
|
||||||
|
timeOut: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 'supercafe', row.id);
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
console.warn('Error checking debts', e);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Object.entries(SC_actions).forEach((category) => {
|
Object.entries(SC_actions).forEach((category) => {
|
||||||
addCategory(
|
addCategory(
|
||||||
@@ -77,23 +107,29 @@ PAGES.supercafe = {
|
|||||||
}
|
}
|
||||||
loadActions();
|
loadActions();
|
||||||
DB.get('supercafe', mid).then((data) => {
|
DB.get('supercafe', mid).then((data) => {
|
||||||
function load_data(data, ENC = "") {
|
function load_data(data, ENC = '') {
|
||||||
document.getElementById(nameh1).innerText = mid;
|
document.getElementById(nameh1).innerText = mid;
|
||||||
document.getElementById(field_fecha).value = data["Fecha"] || CurrentISODate();
|
document.getElementById(field_fecha).value = data['Fecha'] || CurrentISODate();
|
||||||
document.getElementById(field_persona).value = data["Persona"] || "";
|
document.getElementById(field_persona).value = data['Persona'] || '';
|
||||||
currentPersonaID = data["Persona"] || "";
|
currentPersonaID = data['Persona'] || '';
|
||||||
document.getElementById(field_comanda).value =
|
document.getElementById(field_comanda).value =
|
||||||
SC_parse(JSON.parse(data["Comanda"] || "{}")) || "";
|
SC_parse(JSON.parse(data['Comanda'] || '{}')) || '';
|
||||||
document.getElementById(field_notas).value = data["Notas"] || "";
|
document.getElementById(field_notas).value = data['Notas'] || '';
|
||||||
document.getElementById(field_estado).value = data["Estado"] || "%%";
|
document.getElementById(field_estado).value = data['Estado'] || '%%';
|
||||||
currentData = JSON.parse(data["Comanda"] || "{}");
|
currentData = JSON.parse(data['Comanda'] || '{}');
|
||||||
|
|
||||||
loadActions();
|
loadActions();
|
||||||
}
|
}
|
||||||
if (typeof data == "string") {
|
if (typeof data == 'string') {
|
||||||
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
TS_decrypt(
|
||||||
load_data(data, "%E");
|
data,
|
||||||
}, 'supercafe', mid);
|
SECRET,
|
||||||
|
(data, wasEncrypted) => {
|
||||||
|
load_data(data, '%E');
|
||||||
|
},
|
||||||
|
'supercafe',
|
||||||
|
mid
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
load_data(data || {});
|
load_data(data || {});
|
||||||
}
|
}
|
||||||
@@ -104,67 +140,114 @@ PAGES.supercafe = {
|
|||||||
if (guardarBtn.disabled) return;
|
if (guardarBtn.disabled) return;
|
||||||
|
|
||||||
// Validate before disabling button
|
// Validate before disabling button
|
||||||
if (document.getElementById(field_persona).value == "") {
|
if (document.getElementById(field_persona).value == '') {
|
||||||
alert("¡Hay que elegir una persona!");
|
alert('¡Hay que elegir una persona!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var personaId = document.getElementById(field_persona).value;
|
||||||
|
|
||||||
|
// Check for outstanding debts
|
||||||
|
DB.list('supercafe').then((rows) => {
|
||||||
|
var deudasCount = 0;
|
||||||
|
var processed = 0;
|
||||||
|
var total = rows.length;
|
||||||
|
|
||||||
|
if (total === 0) {
|
||||||
|
// No commands, proceed to save
|
||||||
|
proceedToSave();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count debts for this person
|
||||||
|
rows.forEach((row) => {
|
||||||
|
TS_decrypt(row.data, SECRET, (data) => {
|
||||||
|
if (data.Persona == personaId && data.Estado == 'Deuda') {
|
||||||
|
deudasCount++;
|
||||||
|
}
|
||||||
|
processed++;
|
||||||
|
|
||||||
|
// When all rows are processed, check if we can save
|
||||||
|
if (processed === total) {
|
||||||
|
if (deudasCount >= 3) {
|
||||||
|
toastr.error('Esta persona tiene más de 3 comandas en deuda. No se puede realizar el pedido.');
|
||||||
|
// Delete the comanda if it was created
|
||||||
|
if (mid) {
|
||||||
|
DB.del('supercafe', mid).then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setUrlHash('supercafe');
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
proceedToSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 'supercafe', row.id);
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
console.warn('Error checking debts', e);
|
||||||
|
toastr.error('Error al verificar las deudas');
|
||||||
|
});
|
||||||
|
|
||||||
|
function proceedToSave() {
|
||||||
// Disable button after validation passes
|
// Disable button after validation passes
|
||||||
guardarBtn.disabled = true;
|
guardarBtn.disabled = true;
|
||||||
guardarBtn.style.opacity = "0.5";
|
guardarBtn.style.opacity = '0.5';
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
Fecha: document.getElementById(field_fecha).value,
|
Fecha: document.getElementById(field_fecha).value,
|
||||||
Persona: document.getElementById(field_persona).value,
|
Persona: personaId,
|
||||||
Comanda: JSON.stringify(currentData),
|
Comanda: JSON.stringify(currentData),
|
||||||
Notas: document.getElementById(field_notas).value,
|
Notas: document.getElementById(field_notas).value,
|
||||||
Estado: document
|
Estado: document.getElementById(field_estado).value.replace('%%', 'Pedido'),
|
||||||
.getElementById(field_estado)
|
|
||||||
.value.replace("%%", "Pedido"),
|
|
||||||
};
|
};
|
||||||
document.getElementById("actionStatus").style.display = "block";
|
document.getElementById('actionStatus').style.display = 'block';
|
||||||
DB.put('supercafe', mid, data).then(() => {
|
DB.put('supercafe', mid, data)
|
||||||
toastr.success("Guardado!");
|
.then(() => {
|
||||||
|
toastr.success('Guardado!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
setUrlHash("supercafe");
|
setUrlHash('supercafe');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
console.warn('DB.put error', e);
|
console.warn('DB.put error', e);
|
||||||
guardarBtn.disabled = false;
|
guardarBtn.disabled = false;
|
||||||
guardarBtn.style.opacity = "1";
|
guardarBtn.style.opacity = '1';
|
||||||
document.getElementById("actionStatus").style.display = "none";
|
document.getElementById('actionStatus').style.display = 'none';
|
||||||
toastr.error("Error al guardar la comanda");
|
toastr.error('Error al guardar la comanda');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
document.getElementById(btn_borrar).onclick = () => {
|
document.getElementById(btn_borrar).onclick = () => {
|
||||||
if (
|
if (
|
||||||
confirm(
|
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
|
) == true
|
||||||
) {
|
) {
|
||||||
DB.del('supercafe', mid).then(() => {
|
DB.del('supercafe', mid).then(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setUrlHash("supercafe");
|
setUrlHash('supercafe');
|
||||||
}, SAVE_WAIT);
|
}, SAVE_WAIT);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
index: function () {
|
index: function () {
|
||||||
if (!checkRole("supercafe")) {
|
if (!checkRole('supercafe')) {
|
||||||
setUrlHash("index");
|
setUrlHash('index');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var tts = false;
|
var tts = false;
|
||||||
var sc_nobtn = "";
|
var sc_nobtn = '';
|
||||||
if (urlParams.get("sc_nobtn") == "yes") {
|
if (urlParams.get('sc_nobtn') == 'yes') {
|
||||||
sc_nobtn = "pointer-events: none; opacity: 0.5";
|
sc_nobtn = 'pointer-events: none; opacity: 0.5';
|
||||||
}
|
}
|
||||||
var ev = setTimeout(() => {
|
var ev = setTimeout(() => {
|
||||||
tts = true;
|
tts = true;
|
||||||
console.log("TTS Enabled");
|
console.log('TTS Enabled');
|
||||||
toastr.info("Texto a voz disponible");
|
//toastr.info('Texto a voz disponible');
|
||||||
}, 6500);
|
}, 6500);
|
||||||
EventListeners.Timeout.push(ev);
|
EventListeners.Timeout.push(ev);
|
||||||
const tablebody = safeuuid();
|
const tablebody = safeuuid();
|
||||||
@@ -173,58 +256,68 @@ PAGES.supercafe = {
|
|||||||
var totalprecio = safeuuid();
|
var totalprecio = safeuuid();
|
||||||
var tts_check = safeuuid();
|
var tts_check = safeuuid();
|
||||||
var old = {};
|
var old = {};
|
||||||
container.innerHTML = `
|
container.innerHTML = html`
|
||||||
<h1>Cafetería - Total: <span id="${totalprecio}">0</span>c</h1>
|
<h1>Cafetería - Total: <span id="${totalprecio}">0</span>c</h1>
|
||||||
<button id="${btn_new}" style="${sc_nobtn};">Nueva comanda</button>
|
<button id="${btn_new}" style="${sc_nobtn};">Nueva comanda</button>
|
||||||
<br>
|
<br />
|
||||||
<label>
|
<label>
|
||||||
<b>Habilitar avisos:</b>
|
<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>
|
</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>
|
<summary>Todas las comandas</summary>
|
||||||
<div id="cont1"></div>
|
<div id="${tablebody}"></div>
|
||||||
</details>
|
</details>
|
||||||
<br>
|
<br />
|
||||||
<details style="background: lightpink; padding: 15px; border-radius: 15px; border: 2px solid black" open>
|
<details
|
||||||
|
style="background: lightpink; padding: 15px; border-radius: 15px; border: 2px solid black"
|
||||||
|
open
|
||||||
|
>
|
||||||
<summary>Deudas</summary>
|
<summary>Deudas</summary>
|
||||||
<div id="cont2"></div>
|
<div id="${tablebody2}"></div>
|
||||||
</details>
|
</details>
|
||||||
`;
|
`;
|
||||||
|
document.getElementById(tts_check).checked = localStorage.getItem('TELESEC_TTS_ENABLED') === 'true';
|
||||||
|
document.getElementById(tts_check).onchange = function () {
|
||||||
|
localStorage.setItem('TELESEC_TTS_ENABLED', this.checked);
|
||||||
|
}
|
||||||
var config = [
|
var config = [
|
||||||
{
|
{
|
||||||
key: "Persona",
|
key: 'Persona',
|
||||||
type: "persona",
|
type: 'persona',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Persona",
|
label: 'Persona',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Estado",
|
key: 'Estado',
|
||||||
type: "comanda-status",
|
type: 'comanda-status',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Estado",
|
label: 'Estado',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Comanda",
|
key: 'Comanda',
|
||||||
type: "comanda",
|
type: 'comanda',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Comanda",
|
label: 'Comanda',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
if (!checkRole("supercafe:edit")) {
|
if (!checkRole('supercafe:edit')) {
|
||||||
config = [
|
config = [
|
||||||
{
|
{
|
||||||
key: "Persona",
|
key: 'Persona',
|
||||||
type: "persona",
|
type: 'persona',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Persona",
|
label: 'Persona',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Comanda",
|
key: 'Comanda',
|
||||||
type: "comanda",
|
type: 'comanda',
|
||||||
default: "",
|
default: '',
|
||||||
label: "Comanda",
|
label: 'Comanda',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -238,48 +331,72 @@ PAGES.supercafe = {
|
|||||||
document.getElementById(totalprecio).innerText = tot;
|
document.getElementById(totalprecio).innerText = tot;
|
||||||
return tot;
|
return tot;
|
||||||
}
|
}
|
||||||
|
var ttS_data = {};
|
||||||
TS_IndexElement(
|
TS_IndexElement(
|
||||||
"supercafe",
|
'supercafe',
|
||||||
config,
|
config,
|
||||||
"supercafe",
|
'supercafe',
|
||||||
document.querySelector("#cont1"),
|
document.getElementById(tablebody),
|
||||||
(data, new_tr) => {
|
(data, new_tr) => {
|
||||||
// new_tr.style.backgroundColor = "#FFCCCB";
|
// new_tr.style.backgroundColor = "#FFCCCB";
|
||||||
comandasTot[data._key] = SC_priceCalc(JSON.parse(data.Comanda))[0];
|
comandasTot[data._key] = SC_priceCalc(JSON.parse(data.Comanda))[0];
|
||||||
calcPrecio();
|
calcPrecio();
|
||||||
if (data.Estado == "Pedido") {
|
if (data.Estado == 'Pedido') {
|
||||||
new_tr.style.backgroundColor = "#FFFFFF";
|
new_tr.style.backgroundColor = '#FFFFFF';
|
||||||
}
|
}
|
||||||
if (data.Estado == "En preparación") {
|
if (data.Estado == 'En preparación') {
|
||||||
new_tr.style.backgroundColor = "#FFCCCB";
|
new_tr.style.backgroundColor = '#FFCCCB';
|
||||||
}
|
}
|
||||||
if (data.Estado == "Listo") {
|
if (data.Estado == 'Listo') {
|
||||||
new_tr.style.backgroundColor = "gold";
|
new_tr.style.backgroundColor = 'gold';
|
||||||
}
|
}
|
||||||
if (data.Estado == "Entregado") {
|
if (data.Estado == 'Entregado') {
|
||||||
new_tr.style.backgroundColor = "lightgreen";
|
new_tr.style.backgroundColor = 'lightgreen';
|
||||||
}
|
}
|
||||||
if (data.Estado == "Deuda") {
|
if (data.Estado == 'Deuda') {
|
||||||
new_tr.style.backgroundColor = "#f5d3ff";
|
new_tr.style.backgroundColor = '#f5d3ff';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.Estado == "Deuda") {
|
if (data.Estado == 'Deuda') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
var key = data._key;
|
var key = data._key;
|
||||||
if (old[key] == undefined) {
|
if (old[key] == undefined) {
|
||||||
old[key] = "";
|
old[key] = '';
|
||||||
}
|
}
|
||||||
if (old[key] != data.Estado) {
|
if (old[key] != data.Estado) {
|
||||||
if (tts && document.getElementById(tts_check).checked) {
|
if (tts && document.getElementById(tts_check).checked) {
|
||||||
var msg = `Comanda de ${SC_Personas[data.Persona].Region}. - ${
|
if (ttS_data[data.Region] == undefined) {
|
||||||
JSON.parse(data.Comanda)["Selección"]
|
ttS_data[data.Region] = {};
|
||||||
|
}
|
||||||
|
ttS_data[data.Region][data._key] = data.Estado;
|
||||||
|
var allReady = true;
|
||||||
|
Object.values(ttS_data[data.Region]).forEach((estado) => {
|
||||||
|
if (estado != 'Listo') {
|
||||||
|
allReady = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (allReady) {
|
||||||
|
var msgRegion = `Hola, ${SC_Personas[data.Persona].Region}. - Vamos a entregar vuestro pedido. ¡Que aproveche!`;
|
||||||
|
TS_SayTTS(msgRegion)
|
||||||
|
} if (data.Estado == 'Entregado') {
|
||||||
|
var msgEntregado = `El pedido de ${SC_Personas[data.Persona].Nombre} en ${SC_Personas[data.Persona].Region} ha sido entregado.`;
|
||||||
|
TS_SayTTS(msgEntregado)
|
||||||
|
} else if (data.Estado == 'En preparación') {
|
||||||
|
var msgPreparacion = `El pedido de ${SC_Personas[data.Persona].Nombre} en ${SC_Personas[data.Persona].Region} está en preparación.`;
|
||||||
|
TS_SayTTS(msgPreparacion)
|
||||||
|
} else if (data.Estado == 'Listo') {
|
||||||
|
var msgListo = `El pedido de ${SC_Personas[data.Persona].Nombre} en ${SC_Personas[data.Persona].Region} está listo para ser entregado.`;
|
||||||
|
TS_SayTTS(msgListo)
|
||||||
|
} else if (data.Estado == 'Pedido') {
|
||||||
|
var msgPedido = `Se ha realizado un nuevo pedido para ${SC_Personas[data.Persona].Nombre} en ${SC_Personas[data.Persona].Region}.`;
|
||||||
|
TS_SayTTS(msgPedido)
|
||||||
|
} else {
|
||||||
|
var msg = `Comanda de ${SC_Personas[data.Persona].Region}. - ${JSON.parse(data.Comanda)['Selección']
|
||||||
}. - ${SC_Personas[data.Persona].Nombre}. - ${data.Estado}`;
|
}. - ${SC_Personas[data.Persona].Nombre}. - ${data.Estado}`;
|
||||||
let utterance = new SpeechSynthesisUtterance(msg);
|
TS_SayTTS(msg)
|
||||||
utterance.rate = 0.9;
|
}
|
||||||
// utterance.voice = speechSynthesis.getVoices()[7]
|
|
||||||
speechSynthesis.speak(utterance);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
old[key] = data.Estado;
|
old[key] = data.Estado;
|
||||||
@@ -288,58 +405,53 @@ PAGES.supercafe = {
|
|||||||
|
|
||||||
//Deudas
|
//Deudas
|
||||||
TS_IndexElement(
|
TS_IndexElement(
|
||||||
"supercafe",
|
'supercafe',
|
||||||
config,
|
config,
|
||||||
"supercafe",
|
'supercafe',
|
||||||
document.querySelector("#cont2"),
|
document.getElementById(tablebody2),
|
||||||
(data, new_tr) => {
|
(data, new_tr) => {
|
||||||
// new_tr.style.backgroundColor = "#FFCCCB";
|
// new_tr.style.backgroundColor = "#FFCCCB";
|
||||||
comandasTot[data._key] = 0; // No mostrar comandas en deuda.
|
comandasTot[data._key] = 0; // No mostrar comandas en deuda.
|
||||||
calcPrecio();
|
calcPrecio();
|
||||||
|
|
||||||
if (data.Estado == "Pedido") {
|
if (data.Estado == 'Pedido') {
|
||||||
new_tr.style.backgroundColor = "#FFFFFF";
|
new_tr.style.backgroundColor = '#FFFFFF';
|
||||||
}
|
}
|
||||||
if (data.Estado == "En preparación") {
|
if (data.Estado == 'En preparación') {
|
||||||
new_tr.style.backgroundColor = "#FFCCCB";
|
new_tr.style.backgroundColor = '#FFCCCB';
|
||||||
}
|
}
|
||||||
if (data.Estado == "Listo") {
|
if (data.Estado == 'Listo') {
|
||||||
new_tr.style.backgroundColor = "gold";
|
new_tr.style.backgroundColor = 'gold';
|
||||||
}
|
}
|
||||||
if (data.Estado == "Entregado") {
|
if (data.Estado == 'Entregado') {
|
||||||
new_tr.style.backgroundColor = "lightgreen";
|
new_tr.style.backgroundColor = 'lightgreen';
|
||||||
}
|
}
|
||||||
if (data.Estado == "Deuda") {
|
if (data.Estado == 'Deuda') {
|
||||||
new_tr.style.backgroundColor = "#f5d3ff";
|
new_tr.style.backgroundColor = '#f5d3ff';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.Estado != "Deuda") {
|
if (data.Estado != 'Deuda') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
var key = data._key;
|
var key = data._key;
|
||||||
if (old[key] == undefined) {
|
if (old[key] == undefined) {
|
||||||
old[key] = "";
|
old[key] = '';
|
||||||
}
|
}
|
||||||
if (old[key] != data.Estado) {
|
if (old[key] != data.Estado) {
|
||||||
if (tts && document.getElementById(tts_check).checked) {
|
if (tts && document.getElementById(tts_check).checked) {
|
||||||
var msg = `Comanda de ${SC_Personas[data.Persona].Region}. - ${
|
var msg = `La comanda de ${SC_Personas[data.Persona].Nombre} en ${SC_Personas[data.Persona].Region} ha pasado a deuda.`;
|
||||||
JSON.parse(data.Comanda)["Selección"]
|
TS_SayTTS(msg)
|
||||||
}. - ${SC_Personas[data.Persona].Nombre}. - ${data.Estado}`;
|
|
||||||
let utterance = new SpeechSynthesisUtterance(msg);
|
|
||||||
utterance.rate = 0.9;
|
|
||||||
// utterance.voice = speechSynthesis.getVoices()[7]
|
|
||||||
speechSynthesis.speak(utterance);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
old[key] = data.Estado;
|
old[key] = data.Estado;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (!checkRole("supercafe:edit")) {
|
if (!checkRole('supercafe:edit')) {
|
||||||
document.getElementById(btn_new).style.display = "none";
|
document.getElementById(btn_new).style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
document.getElementById(btn_new).onclick = () => {
|
document.getElementById(btn_new).onclick = () => {
|
||||||
setUrlHash("supercafe," + safeuuid(""));
|
setUrlHash('supercafe,' + safeuuid(''));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
182
src/pwa.js
@@ -1,43 +1,195 @@
|
|||||||
let newWorker;
|
let newWorker;
|
||||||
|
const APP_VERSION = '%%VERSIONCO%%';
|
||||||
|
const VERSION_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
||||||
|
let lastVersionCheckTs = 0;
|
||||||
|
let updatePromptShown = false;
|
||||||
|
|
||||||
|
function sendCouchUrlPrefixToServiceWorker(registration) {
|
||||||
|
if (!registration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const couchUrlPrefix = (localStorage.getItem('TELESEC_COUCH_URL') || '').trim();
|
||||||
|
const message = {
|
||||||
|
type: 'SET_COUCH_URL_PREFIX',
|
||||||
|
url: couchUrlPrefix
|
||||||
|
};
|
||||||
|
|
||||||
|
if (registration.active) {
|
||||||
|
registration.active.postMessage(message);
|
||||||
|
}
|
||||||
|
if (registration.waiting) {
|
||||||
|
registration.waiting.postMessage(message);
|
||||||
|
}
|
||||||
|
if (registration.installing) {
|
||||||
|
registration.installing.postMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAppVersion(force = false) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (!force && now - lastVersionCheckTs < VERSION_CHECK_INTERVAL_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!navigator.onLine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastVersionCheckTs = now;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/version.json?t=${Date.now()}`, {
|
||||||
|
cache: 'no-cache'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data || !data.version) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.version !== APP_VERSION) {
|
||||||
|
const registration = await navigator.serviceWorker.getRegistration();
|
||||||
|
if (registration) {
|
||||||
|
await registration.update();
|
||||||
|
}
|
||||||
|
if (!updatePromptShown) {
|
||||||
|
showUpdateBar();
|
||||||
|
updatePromptShown = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updatePromptShown = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('No se pudo comprobar la versión remota:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ActualizarProgramaTeleSec() {
|
||||||
|
if (!confirm('Se borrará la caché local del programa y se recargará la aplicación. ¿Continuar?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cacheCleared = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
const registration = await navigator.serviceWorker.getRegistration();
|
||||||
|
if (registration) {
|
||||||
|
await registration.update();
|
||||||
|
const sendSkipWaiting = (worker) => {
|
||||||
|
worker.postMessage({ type: 'SKIP_WAITING' });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (registration.waiting) {
|
||||||
|
sendSkipWaiting(registration.waiting);
|
||||||
|
} else if (registration.installing) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
const onStateChange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
installingWorker.removeEventListener('statechange', onStateChange);
|
||||||
|
sendSkipWaiting(installingWorker);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
installingWorker.addEventListener('statechange', onStateChange);
|
||||||
|
onStateChange();
|
||||||
|
setTimeout(resolve, 2500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('caches' in window) {
|
||||||
|
const cacheNames = await caches.keys();
|
||||||
|
await Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
cacheCleared = false;
|
||||||
|
console.error('No se pudo limpiar la caché completamente:', error);
|
||||||
|
if (typeof toastr !== 'undefined') {
|
||||||
|
toastr.error('No se pudo limpiar toda la caché. Recargando igualmente...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheCleared && typeof toastr !== 'undefined') {
|
||||||
|
toastr.success('Caché limpiada. Recargando aplicación...');
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
location.reload();
|
||||||
|
}, 700);
|
||||||
|
}
|
||||||
function showUpdateBar() {
|
function showUpdateBar() {
|
||||||
let snackbar = document.getElementById("snackbar");
|
let snackbar = document.getElementById('snackbar');
|
||||||
snackbar.className = "show";
|
snackbar.className = 'show';
|
||||||
}
|
}
|
||||||
|
|
||||||
// The click event on the pop up notification
|
// The click event on the pop up notification
|
||||||
document.getElementById("reload").addEventListener("click", function () {
|
document.getElementById('reload').addEventListener('click', function () {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
removeCache();
|
ActualizarProgramaTeleSec();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
newWorker.postMessage({ action: "skipWaiting" });
|
if (newWorker) {
|
||||||
|
newWorker.postMessage({ type: 'SKIP_WAITING' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if ("serviceWorker" in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register("sw.js").then((reg) => {
|
const wireRegistration = (reg) => {
|
||||||
reg.addEventListener("updatefound", () => {
|
reg.addEventListener('updatefound', () => {
|
||||||
// A wild service worker has appeared in reg.installing!
|
|
||||||
newWorker = reg.installing;
|
newWorker = reg.installing;
|
||||||
|
|
||||||
newWorker.addEventListener("statechange", () => {
|
newWorker.addEventListener('statechange', () => {
|
||||||
// Has network.state changed?
|
|
||||||
switch (newWorker.state) {
|
switch (newWorker.state) {
|
||||||
case "installed":
|
case 'installed':
|
||||||
if (navigator.serviceWorker.controller) {
|
if (navigator.serviceWorker.controller) {
|
||||||
// new update available
|
|
||||||
showUpdateBar();
|
showUpdateBar();
|
||||||
}
|
}
|
||||||
// No update available
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
navigator.serviceWorker.getRegistration().then(async (reg) => {
|
||||||
|
if (!reg) {
|
||||||
|
reg = await navigator.serviceWorker.register('sw.js');
|
||||||
|
} else {
|
||||||
|
await reg.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
wireRegistration(reg);
|
||||||
|
sendCouchUrlPrefixToServiceWorker(reg);
|
||||||
|
checkAppVersion(true);
|
||||||
|
setInterval(checkAppVersion, VERSION_CHECK_INTERVAL_MS);
|
||||||
});
|
});
|
||||||
|
|
||||||
let refreshing;
|
let refreshing;
|
||||||
navigator.serviceWorker.addEventListener("controllerchange", function () {
|
navigator.serviceWorker.addEventListener('controllerchange', function () {
|
||||||
if (refreshing) return;
|
if (refreshing) return;
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
refreshing = true;
|
refreshing = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
checkAppVersion();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('storage', (event) => {
|
||||||
|
if (event.key !== 'TELESEC_COUCH_URL') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.serviceWorker.getRegistration().then((registration) => {
|
||||||
|
sendCouchUrlPrefixToServiceWorker(registration);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
43
src/sw.js
@@ -1,12 +1,29 @@
|
|||||||
var CACHE = "telesec_%%VERSIONCO%%";
|
var CACHE = 'telesec_%%VERSIONCO%%';
|
||||||
importScripts(
|
importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js');
|
||||||
"https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js"
|
|
||||||
);
|
|
||||||
|
|
||||||
self.addEventListener("message", (event) => {
|
let couchUrlPrefix = '';
|
||||||
if (event.data && event.data.type === "SKIP_WAITING") {
|
|
||||||
|
function normalizePrefix(url) {
|
||||||
|
if (!url || typeof url !== 'string') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedUrl = url.trim();
|
||||||
|
if (!trimmedUrl) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedUrl.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.data && event.data.type === 'SET_COUCH_URL_PREFIX') {
|
||||||
|
couchUrlPrefix = normalizePrefix(event.data.url);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// workbox.routing.registerRoute(
|
// workbox.routing.registerRoute(
|
||||||
@@ -18,9 +35,17 @@ self.addEventListener("message", (event) => {
|
|||||||
|
|
||||||
// All but couchdb
|
// All but couchdb
|
||||||
workbox.routing.registerRoute(
|
workbox.routing.registerRoute(
|
||||||
({ url }) =>
|
({ request, url }) => {
|
||||||
!url.pathname.startsWith("/_couchdb/") && url.origin === self.location.origin,
|
const requestUrl = request && request.url ? request.url : url.href;
|
||||||
new workbox.strategies.NetworkFirst({
|
const normalizedRequestUrl = normalizePrefix(requestUrl);
|
||||||
|
|
||||||
|
if (couchUrlPrefix && normalizedRequestUrl.startsWith(couchUrlPrefix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !url.pathname.startsWith('/_couchdb/') && url.origin === self.location.origin;
|
||||||
|
},
|
||||||
|
new workbox.strategies.CacheFirst({
|
||||||
cacheName: CACHE,
|
cacheName: CACHE,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
3
src/version.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"version": "%%VERSIONCO%%"
|
||||||
|
}
|
||||||