16 Commits

Author SHA1 Message Date
Naiel
c940c846b8 Mejorar la presentación de la transacción en la página de pagos y optimizar la carga de datos 2026-03-04 23:02:26 +00:00
Naiel
7822e46b61 Añadir funcionalidad para gestionar puntos de interés en Gest-Aula 2026-03-04 14:45:04 +00:00
Naiel
a39ecca990 Mejoras generales, rediseño de Gest-Aula, borrado de modulos sin usar y Mensajes, movido el resumen diario a gest-aula. 2026-03-04 14:33:28 +00:00
Naiel
80e9262bcb fix logic 2026-03-03 11:33:23 +01:00
Naiel
cd456ab9f1 fix 2026-03-03 11:29:38 +01:00
Naiel
c0c40ecd99 fix 2026-03-03 11:24:19 +01:00
Naiel
98c6ba39f3 fix 2026-03-03 11:20:57 +01:00
Naiel
03f52c8a92 fix 2026-03-03 11:06:30 +01:00
Naiel
f655a736b3 fix 2026-03-03 10:59:02 +01:00
Naiel
89a68f27da fix 2026-03-03 10:50:05 +01:00
Naiel
4d322e5696 fix 2026-03-03 10:36:42 +01:00
Naiel
0138e0ca69 fix 2026-03-03 10:33:36 +01:00
Naiel
3e8542c9de Fix import error handling for telesec_couchdb 2026-03-03 10:27:04 +01:00
Naiel
90df81d308 Update asset path for GitHub Release upload 2026-03-03 10:24:12 +01:00
Naiel
53941da35c Update Windows agent release workflow for PyInstaller 2026-03-03 10:21:47 +01:00
Naiel
105c911c59 feat: añadir soporte para configuración JSON en el agente de Windows 2026-03-02 12:47:02 +00:00
14 changed files with 1141 additions and 1021 deletions

View File

@@ -31,16 +31,16 @@ jobs:
- name: Build hidden EXE with PyInstaller
shell: bash
run: |
pyinstaller --noconfirm --clean --onefile --noconsole --name telesec-windows-agent python_sdk/windows_agent.py
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: dist/telesec-windows-agent.exe
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: dist/telesec-windows-agent.exe
files: python_sdk/dist/telesec-windows-agent.exe

View File

@@ -57,20 +57,37 @@ Se añadió soporte para control de ordenadores del aula:
### 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 \
--server "https://tu-couchdb" \
--db "telesec" \
--user "usuario" \
--password "clave" \
--secret "SECRET123"
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)

View File

@@ -65,9 +65,11 @@ a:hover {
@media print {
.supermesh-indicator,
.no_print,
.no_print * {
.no_print *,
.saveico, .delico, .opicon {
display: none !important;
}
main {padding: 0;}
}
button,

View File

@@ -1,3 +0,0 @@
from .telesec_couchdb import TeleSecCouchDB, ts_encrypt, ts_decrypt
__all__ = ["TeleSecCouchDB", "ts_encrypt", "ts_decrypt"]

View File

@@ -76,52 +76,32 @@ def ts_encrypt(input_value: Any, secret: str) -> str:
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.
"""
def ts_encrypt(input_value: Any, secret: str) -> str:
if not isinstance(input_value, str):
return input_value
payload = json.dumps(input_value, separators=(",", ":"), ensure_ascii=False)
else:
payload = 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
payload_bytes = payload.encode("utf-8")
salt = os.urandom(8)
if len(raw) < 16 or not raw.startswith(b"Salted__"):
raise TeleSecCryptoError("Unsupported encrypted payload format")
# OpenSSL EVP_BytesToKey (MD5)
dx = b""
salted = b""
while len(salted) < 48: # 32 key + 16 iv
dx = hashlib.md5(dx + secret.encode() + salt).digest()
salted += dx
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)
key = salted[:32]
iv = salted[32:48]
try:
text = decrypted.decode("utf-8")
except UnicodeDecodeError:
text = decrypted.decode("latin-1")
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(_pkcs7_pad(payload_bytes, 16))
try:
return json.loads(text)
except Exception:
return text
try:
return json.loads(input_value)
except Exception:
return input_value
openssl_blob = b"Salted__" + salt + encrypted
b64 = base64.b64encode(openssl_blob).decode("utf-8")
return f"RSA{{{b64}}}"
@dataclass
class TeleSecDoc:

View File

@@ -1,21 +1,305 @@
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
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
try:
from .telesec_couchdb import TeleSecCouchDB, TeleSecCouchDBError, ts_decrypt
except ImportError:
from telesec_couchdb import TeleSecCouchDB, TeleSecCouchDBError, ts_decrypt
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")
@@ -88,7 +372,7 @@ 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
return server_now <= target
def execute_shutdown(dry_run: bool = False) -> None:
@@ -130,28 +414,113 @@ def run_once(client: TeleSecCouchDB, machine_id: str, dry_run: bool = False) ->
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="TeleSec Windows Agent")
parser.add_argument("--server", required=True, help="CouchDB server URL, ej. https://couch.example")
parser.add_argument("--db", default="telesec", help="Database name")
parser.add_argument("--server", default="", help="CouchDB server URL, ej. https://couch.example")
parser.add_argument("--db", default="", help="Database name")
parser.add_argument("--user", default="", help="CouchDB username")
parser.add_argument("--password", default="", help="CouchDB password")
parser.add_argument("--secret", required=True, help="TeleSec secret para cifrado")
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()
machine_id = (args.machine_id or socket.gethostname() or "unknown-host").strip()
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=args.server,
dbname=args.db,
secret=args.secret,
username=args.user or None,
password=args.password or None,
server_url=server,
dbname=db,
secret=secret,
username=user or None,
password=password or None,
)
try:
@@ -169,7 +538,7 @@ def main() -> int:
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, args.interval))
time.sleep(max(5, int(interval)))
if __name__ == "__main__":

View File

@@ -949,9 +949,11 @@ function TS_decrypt(input, secret, callback, table, id) {
}
// Encrypted format marker: RSA{<ciphertext>} where <ciphertext> is CryptoJS AES output
// console.debug(input);
if (input.startsWith('RSA{') && input.endsWith('}') && typeof CryptoJS !== 'undefined') {
try {
var data = input.slice(4, -1);
// console.debug("TS_decrypt secret:", ">" + secret + "<", typeof secret, secret?.length);
var words = CryptoJS.AES.decrypt(data, secret);
var decryptedUtf8 = null;
try {

View File

@@ -85,15 +85,11 @@
<script src="page/dataman.js"></script>
<script src="page/aulas.js"></script>
<script src="page/materiales.js"></script>
<script src="page/resumen_diario.js"></script>
<script src="page/personas.js"></script>
<script src="page/supercafe.js"></script>
<!-- <script src="page/avisos.js"></script> -->
<script src="page/comedor.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/buscar.js"></script>
<script src="page/pagos.js"></script>
</body>

View File

@@ -1,4 +1,6 @@
PERMS['aulas'] = 'Aulas (Solo docentes!)';
PERMS['aulas:resumen_diario'] = '> Resumen diario';
PERMS['aulas:puntos_interes'] = '> Puntos de interés';
PAGES.aulas = {
//navcss: "btn1",
Title: 'Gest-Aula',
@@ -13,168 +15,102 @@ PAGES.aulas = {
var data_Tareas = safeuuid();
var data_Diario = safeuuid();
var data_Weather = safeuuid();
var link_alertas = safeuuid();
var link_diario = safeuuid();
var link_actividades = safeuuid();
var link_puntos_interes = safeuuid();
container.innerHTML = html`
<h1>Gestión del Aula</h1>
<div>
<fieldset style="float: left;">
<legend><img src="${PAGES.notas.icon}" height="20" /> Notas esenciales</legend>
<a class="button" style="font-size: 25px;" href="#notas,inicio_dia"
>Como iniciar el día</a
>
<a class="button" style="font-size: 25px;" href="#notas,realizacion_cafe"
>Como realizar el café</a
>
<a class="button" style="font-size: 25px;" href="#notas,fin_dia">Como acabar el día</a>
<a class="button" style="font-size: 25px;" href="#notas,horario">Horario</a>
<a class="button" style="font-size: 25px;" href="#notas,tareas">Tareas</a>
<legend>Atajos de hoy</legend>
<a class="button" id="${link_alertas}" href="#notas,alertas">
Alertas
</a>
<a class="button btn2" href="#aulas,solicitudes,${safeuuid('')}">
Solicitar material
</a>
<a class="button" id="${link_diario}" href="#aulas,informes,diario-${CurrentISODate()}">
Informe
</a>
<a class="button" id="${link_actividades}" href="#aulas,informes,actividades-${CurrentISODate()}">
Actividades
</a>
<a class="button btn5" href="#aulas,ordenadores">
Ordenadores
</a>
<a class="button btn6" href="#aulas,resumen_diario">
Resumen Diario
</a>
</fieldset>
<fieldset style="float: left;">
<legend>Acciones</legend>
<a class="button" style="font-size: 25px;" href="#aulas,solicitudes"
><img src="${PAGES.materiales.icon}" height="20" /> Solicitudes de material</a
>
<a
class="button"
style="font-size: 25px;"
href="#aulas,informes,diario-${CurrentISODate()}"
>Diario de hoy</a
>
<a class="button rojo" style="font-size: 25px;" href="#notas,alertas"
><img src="${PAGES.notas.icon}" height="20" /> Ver Alertas</a
>
<a class="button" style="font-size: 25px;" href="#aulas,informes"
><img src="${PAGES.aulas.icon}" height="20" /> Informes y diarios</a
>
<a class="button btn4" style="font-size: 25px;" href="#supercafe"
><img src="${PAGES.supercafe.icon}" height="20" /> Ver comandas</a
>
<a class="button btn8" style="font-size: 25px;" href="#aulas,ordenadores"
><img src="${PAGES.aulas.icon}" height="20" /> Control de ordenadores</a
>
</fieldset>
<fieldset style="float: left;">
<legend>Datos de hoy</legend>
<span
class="btn7"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"
><b>Menú Comedor:</b> <br /><span id="${data_Comedor}">Cargando...</span></span
>
<span
class="btn6"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"
><b>Tareas:</b> <br />
<pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Tareas}">
Cargando...</pre
>
</span>
<span
class="btn5"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"
><b>Diario:</b> <br />
<pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Diario}">
Cargando...</pre
>
</span>
<span
class="btn4"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"
><b>Clima:</b> <br /><img
loading="lazy"
style="padding: 15px; background-color: white; width: 245px;"
id="${data_Weather}"
/></span>
<a class="button" style="font-size: 25px;" href="#aulas,solicitudes">
Solicitudes de materiales
</a>
<a class="button" style="font-size: 25px;" href="#aulas,informes">
Informes
</a>
<a class="button btn8" style="font-size: 25px;" href="#aulas,puntos_interes">
Puntos de interés
</a>
</fieldset>
</div>
`;
//#region Cargar Clima
// Get location from DB settings.weather_location; if missing ask user and save it
// url format: https://wttr.in/<loc>?F0m
DB.get('settings', 'weather_location').then((loc) => {
if (!loc) {
loc = prompt('Introduce tu ubicación para el clima (ciudad, país):', 'Madrid, Spain');
if (loc) {
DB.put('settings', 'weather_location', loc);
//#region Contar alertas activas y mostrarlas en el botón
DB.get('notas', 'alertas')
.then((res) => TS_decrypt(res, SECRET, (data) => {
var count = 0;
// Sumar el total de alertas activas, cada linea de "Contenido"
// es una alerta, aunque podrían hacerse varias por nota.
// Ignora lineas que no empiezen por > (por si el profesor escribe algo que no es una alerta)
data.Contenido.split('\n').forEach((line) => {
if (line.trim().startsWith('>')) count++;
});
if (count > 0) {
document.getElementById(link_alertas).innerText = `Alertas (${count})`;
document.getElementById(link_alertas).classList.add('rojo');
} else {
document.getElementById(link_alertas).innerText = 'Alertas';
document.getElementById(link_alertas).classList.remove('rojo');
}
}
if (loc) {
document.getElementById(data_Weather).src =
'https://wttr.in/' + encodeURIComponent(loc) + '_IF0m_background=FFFFFF.png';
}))
.catch((e) => {
console.warn('Error contando alertas activas', e);
});
//#endregion Contar alertas activas
//#region Comprobar si hay un diario para hoy y marcar el botón
DB.get('aulas_informes', 'diario-' + CurrentISODate())
.then((res) => {
if (res) {
document.getElementById(link_diario).classList.add('btn2');
} else {
document.getElementById(link_diario).classList.remove('btn2');
}
})
.catch((e) => {
console.warn('Error comprobando diario de hoy', e);
});
//#endregion Comprobar diario
//#region Comprobar si hay un informe de actividades para hoy y contar las actividades (mismo formato que alertas)
DB.get('aulas_informes', 'actividades-' + CurrentISODate()).then((res) => TS_decrypt(res, SECRET, (data) => {
var count = 0;
data.Contenido.split('\n').forEach((line) => {
if (line.trim().startsWith('>')) count++;
});
if (count > 0) {
document.getElementById(link_actividades).innerText = `Actividades (${count})`;
document.getElementById(link_actividades).classList.add('btn4');
} else {
document.getElementById(data_Weather).src = 'https://wttr.in/_IF0m_background=FFFFFF.png';
document.getElementById(link_actividades).innerText = 'Actividades';
document.getElementById(link_actividades).classList.remove('btn4');
}
}))
.catch((e) => {
console.warn('Error comprobando actividades de hoy', e);
});
//#endregion Cargar Clima
//#region Cargar Comedor
DB.get('comedor', CurrentISODate()).then((data) => {
function add_row(data) {
// Fix newlines
data.Platos = data.Platos || 'No hay platos registrados para hoy.';
// Display platos
document.getElementById(data_Comedor).innerHTML = data.Platos.replace(/\n/g, '<br>');
}
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'comedor',
CurrentISODate()
);
} else {
add_row(data || {});
}
});
//#endregion Cargar Comedor
//#region Cargar Tareas
DB.get('notas', 'tareas').then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || 'No hay tareas.';
// Display platos
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(/\n/g, '<br>');
}
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'notas',
'tareas'
);
} else {
add_row(data || {});
}
});
//#endregion Cargar Tareas
//#region Cargar Diario
DB.get('aulas_informes', 'diario-' + CurrentISODate()).then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || 'No hay un diario.';
// Display platos
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(/\n/g, '<br>');
}
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'aulas_informes',
'diario-' + CurrentISODate()
);
} else {
add_row(data || {});
}
});
//#endregion Cargar Diario
//#endregion Comprobar actividades
},
_solicitudes: function () {
const tablebody = safeuuid();
@@ -190,7 +126,7 @@ Cargando...</pre
[
{
key: 'Solicitante',
type: 'persona',
type: 'persona-nombre',
default: '',
label: 'Solicitante',
},
@@ -233,8 +169,14 @@ Cargando...</pre
><br /><br />
</label>
<hr />
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
<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>
</fieldset>
`;
(async () => {
@@ -311,7 +253,7 @@ Cargando...</pre
<div
style="display: inline-block; border: 2px solid black; padding: 5px; border-radius: 5px;"
>
<b>Diario:</b><br />
<b>Por fecha:</b><br />
<input type="date" id="${field_new_byday}" value="${CurrentISODate()}" />
<button id="${btn_new_byday}">Abrir / Nuevo</button>
</div>
@@ -324,7 +266,7 @@ Cargando...</pre
[
{
key: 'Autor',
type: 'persona',
type: 'persona-nombre',
default: '',
label: 'Autor',
},
@@ -367,7 +309,10 @@ Cargando...</pre
var title = '';
if (mid.startsWith('diario-')) {
var date = mid.replace('diario-', '').split('-');
title = 'Diario ' + date[2] + '/' + date[1] + '/' + date[0];
title = 'Informe del ' + date[2] + '/' + date[1] + '/' + date[0];
} else if (mid.startsWith('actividades-')) {
var date = mid.replace('actividades-', '').split('-');
title = 'Actividades para el ' + date[2] + '/' + date[1] + '/' + date[0];
}
container.innerHTML = html`
<a class="button" href="#aulas,informes">← Volver a informes</a>
@@ -388,8 +333,14 @@ Cargando...</pre
><br /><br />
</label>
<hr />
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
<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>
</fieldset>
`;
(async () => {
@@ -471,6 +422,54 @@ Cargando...</pre
);
});
},
__leafletPromise: null,
__ensureLeaflet: function () {
if (window.L && typeof window.L.map === 'function') {
return Promise.resolve(window.L);
}
if (PAGES.aulas.__leafletPromise) {
return PAGES.aulas.__leafletPromise;
}
PAGES.aulas.__leafletPromise = new Promise((resolve, reject) => {
try {
if (!document.getElementById('telesec-leaflet-css')) {
var css = document.createElement('link');
css.id = 'telesec-leaflet-css';
css.rel = 'stylesheet';
css.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
css.integrity = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=';
css.crossOrigin = '';
document.head.appendChild(css);
}
if (window.L && typeof window.L.map === 'function') {
resolve(window.L);
return;
}
var existing = document.getElementById('telesec-leaflet-js');
if (existing) {
existing.addEventListener('load', () => resolve(window.L));
existing.addEventListener('error', () => reject(new Error('No se pudo cargar Leaflet')));
return;
}
var script = document.createElement('script');
script.id = 'telesec-leaflet-js';
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
script.integrity = 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=';
script.crossOrigin = '';
script.onload = () => resolve(window.L);
script.onerror = () => reject(new Error('No se pudo cargar Leaflet'));
document.body.appendChild(script);
} catch (e) {
reject(e);
}
});
return PAGES.aulas.__leafletPromise;
},
__getServerNow: async function () {
try {
var couchUrl = (localStorage.getItem('TELESEC_COUCH_URL') || '').replace(/\/$/, '');
@@ -659,6 +658,510 @@ Cargando...</pre
await loadData();
};
},
_puntos_interes: function () {
var map_id = safeuuid();
var btn_new = safeuuid();
var btn_my_gps = safeuuid();
container.innerHTML = html`
<a class="button" href="#aulas">← Volver a Gestión de Aulas</a>
<h1>Puntos de interés</h1>
<p>Registra ubicaciones (tiendas, bares, entidades, etc.) y visualízalas en el mapa.</p>
<button id="${btn_new}">Nuevo punto</button>
<button class="btn5" id="${btn_my_gps}">Centrar en mi GPS</button>
<div id="${map_id}" style="height: 380px; border: 2px solid black; margin-top: 8px; border-radius: 8px;"></div>
<div id="cont"></div>
`;
var map = null;
var layer = null;
var markers = {};
function parseCoord(value) {
if (value === null || value === undefined || value === '') return null;
var parsed = parseFloat(String(value).replace(',', '.'));
return isNaN(parsed) ? null : parsed;
}
function updateMarker(data) {
if (!map || !layer || !data || !data._key) return;
var lat = parseCoord(data.Latitud);
var lng = parseCoord(data.Longitud);
if (lat === null || lng === null) {
if (markers[data._key]) {
layer.removeLayer(markers[data._key]);
delete markers[data._key];
}
return;
}
var popup = '<b>' + (data.Nombre || data._key) + '</b>';
if (data.Tipo) popup += '<br>' + data.Tipo;
if (data.Direccion) popup += '<br>' + data.Direccion;
if (markers[data._key]) {
markers[data._key].setLatLng([lat, lng]).bindPopup(popup);
} else {
markers[data._key] = L.marker([lat, lng]).addTo(layer).bindPopup(popup);
}
}
function removeMarker(key) {
if (!layer) return;
if (markers[key]) {
layer.removeLayer(markers[key]);
delete markers[key];
}
}
function focusPoint(data) {
if (!map) return;
var lat = parseCoord(data.Latitud);
var lng = parseCoord(data.Longitud);
if (lat === null || lng === null) {
toastr.error('Este punto no tiene coordenadas válidas');
return;
}
map.setView([lat, lng], 17);
if (markers[data._key]) {
markers[data._key].openPopup();
}
}
TS_IndexElement(
'aulas,puntos_interes',
[
{ key: 'Nombre', type: 'raw', default: '', label: 'Nombre' },
{ key: 'Tipo', type: 'raw', default: '', label: 'Tipo' },
{ key: 'Direccion', type: 'raw', default: '', label: 'Dirección' },
{
key: 'Latitud',
type: 'template',
label: 'Ubicación',
template: (data, td) => {
var lat = parseCoord(data.Latitud);
var lng = parseCoord(data.Longitud);
if (lat === null || lng === null) {
td.innerText = 'Sin coordenadas';
return;
}
var txt = document.createElement('div');
txt.innerText = lat.toFixed(6) + ', ' + lng.toFixed(6);
td.appendChild(txt);
var btn = document.createElement('button');
btn.className = 'btn6';
btn.innerText = 'Ver en mapa';
btn.onclick = (ev) => {
ev.preventDefault();
ev.stopPropagation();
focusPoint(data);
return false;
};
td.appendChild(btn);
},
},
],
'aulas_puntos_interes',
document.querySelector('#cont')
);
document.getElementById(btn_new).onclick = () => {
setUrlHash('aulas,puntos_interes,' + safeuuid(''));
};
document.getElementById(btn_my_gps).onclick = () => {
if (!navigator.geolocation) {
toastr.error('Tu navegador no soporta geolocalización');
return;
}
navigator.geolocation.getCurrentPosition(
(pos) => {
if (!map) return;
map.setView([pos.coords.latitude, pos.coords.longitude], 16);
L.circleMarker([pos.coords.latitude, pos.coords.longitude], {
radius: 8,
color: '#2b8a3e',
fillColor: '#2b8a3e',
fillOpacity: 0.7,
})
.addTo(map)
.bindPopup('Tu ubicación actual')
.openPopup();
},
(err) => {
console.warn('Error obteniendo GPS', err);
toastr.error('No se pudo obtener la ubicación GPS');
},
{ enableHighAccuracy: true, timeout: 10000 }
);
};
(async () => {
try {
await PAGES.aulas.__ensureLeaflet();
map = L.map(map_id).setView([40.4168, -3.7038], 6);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; OpenStreetMap',
}).addTo(map);
layer = L.layerGroup().addTo(map);
EventListeners.DB.push(
DB.map('aulas_puntos_interes', (raw, key) => {
if (raw === null) {
removeMarker(key);
return;
}
PAGES.aulas
.__decryptIfNeeded('aulas_puntos_interes', key, raw)
.then((data) => {
data = data || {};
data._key = key;
updateMarker(data);
})
.catch((e) => {
console.warn('Error cargando punto de interés para mapa', e);
});
})
);
} catch (e) {
console.warn('Leaflet no disponible', e);
toastr.error('No se pudo cargar el mapa');
}
})();
},
_puntos_interes__edit: function (mid) {
var field_nombre = safeuuid();
var field_tipo = safeuuid();
var field_direccion = safeuuid();
var field_descripcion = safeuuid();
var field_lat = safeuuid();
var field_lng = safeuuid();
var btn_gps = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var map_id = safeuuid();
container.innerHTML = html`
<a class="button" href="#aulas,puntos_interes">← Volver a puntos de interés</a>
<h1>Punto de interés <code>${mid}</code></h1>
<fieldset style="float: none; width: calc(100% - 40px); max-width: none;">
<legend>Datos</legend>
<label>Nombre<br /><input type="text" id="${field_nombre}" /></label><br /><br />
<label>Tipo (tienda, bar, entidad...)<br /><input type="text" id="${field_tipo}" /></label><br /><br />
<label>Dirección<br /><input type="text" id="${field_direccion}" style="width: calc(100% - 20px);" /></label><br /><br />
<label>Descripción<br /><textarea id="${field_descripcion}" style="width: 100%; height: 120px;"></textarea></label><br /><br />
<div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: end;">
<label>Latitud<br /><input type="text" id="${field_lat}" placeholder="40.4168" /></label>
<label>Longitud<br /><input type="text" id="${field_lng}" placeholder="-3.7038" /></label>
<button class="btn5" id="${btn_gps}">Usar GPS</button>
</div>
<br />
<div id="${map_id}" style="height: 360px; border: 2px solid black; border-radius: 8px;"></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>
</fieldset>
`;
var map = null;
var marker = null;
function parseCoord(value) {
if (value === null || value === undefined || value === '') return null;
var parsed = parseFloat(String(value).replace(',', '.'));
return isNaN(parsed) ? null : parsed;
}
function setCoordInputs(lat, lng) {
document.getElementById(field_lat).value =
lat === null || lat === undefined ? '' : Number(lat).toFixed(6);
document.getElementById(field_lng).value =
lng === null || lng === undefined ? '' : Number(lng).toFixed(6);
}
function refreshMarker(centerMap = false) {
if (!map) return;
var lat = parseCoord(document.getElementById(field_lat).value);
var lng = parseCoord(document.getElementById(field_lng).value);
if (lat === null || lng === null) {
if (marker) {
map.removeLayer(marker);
marker = null;
}
return;
}
if (!marker) {
marker = L.marker([lat, lng], { draggable: true }).addTo(map);
marker.on('dragend', (ev) => {
var ll = ev.target.getLatLng();
setCoordInputs(ll.lat, ll.lng);
});
} else {
marker.setLatLng([lat, lng]);
}
if (centerMap) {
map.setView([lat, lng], 17);
}
}
(async () => {
try {
await PAGES.aulas.__ensureLeaflet();
map = L.map(map_id).setView([40.4168, -3.7038], 6);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; OpenStreetMap',
}).addTo(map);
map.on('click', (ev) => {
setCoordInputs(ev.latlng.lat, ev.latlng.lng);
refreshMarker(false);
});
var raw = await DB.get('aulas_puntos_interes', mid);
var data = await PAGES.aulas.__decryptIfNeeded('aulas_puntos_interes', mid, raw);
data = data || {};
document.getElementById(field_nombre).value = data.Nombre || '';
document.getElementById(field_tipo).value = data.Tipo || '';
document.getElementById(field_direccion).value = data.Direccion || '';
document.getElementById(field_descripcion).value = data.Descripcion || '';
setCoordInputs(parseCoord(data.Latitud), parseCoord(data.Longitud));
refreshMarker(true);
} catch (e) {
console.warn('Error iniciando mapa de punto de interés', e);
toastr.error('No se pudo cargar el mapa del punto de interés');
}
})();
document.getElementById(field_lat).addEventListener('input', () => refreshMarker(false));
document.getElementById(field_lng).addEventListener('input', () => refreshMarker(false));
document.getElementById(btn_gps).onclick = (ev) => {
ev.preventDefault();
if (!navigator.geolocation) {
toastr.error('Tu navegador no soporta geolocalización');
return false;
}
navigator.geolocation.getCurrentPosition(
(pos) => {
setCoordInputs(pos.coords.latitude, pos.coords.longitude);
refreshMarker(true);
toastr.success('Ubicación GPS capturada');
},
(err) => {
console.warn('Error GPS', err);
toastr.error('No se pudo obtener tu ubicación GPS');
},
{ enableHighAccuracy: true, timeout: 10000 }
);
return false;
};
document.getElementById(btn_guardar).onclick = () => {
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
var lat = parseCoord(document.getElementById(field_lat).value);
var lng = parseCoord(document.getElementById(field_lng).value);
var hasLat = document.getElementById(field_lat).value.trim() !== '';
var hasLng = document.getElementById(field_lng).value.trim() !== '';
if ((hasLat && lat === null) || (hasLng && lng === null) || (hasLat !== hasLng)) {
toastr.error('Introduce latitud y longitud válidas');
return;
}
guardarBtn.disabled = true;
guardarBtn.style.opacity = '0.5';
var data = {
Nombre: document.getElementById(field_nombre).value,
Tipo: document.getElementById(field_tipo).value,
Direccion: document.getElementById(field_direccion).value,
Descripcion: document.getElementById(field_descripcion).value,
Latitud: lat === null ? '' : Number(lat).toFixed(6),
Longitud: lng === null ? '' : Number(lng).toFixed(6),
UpdatedAt: new Date().toISOString(),
Autor: SUB_LOGGED_IN_ID || '',
};
document.getElementById('actionStatus').style.display = 'block';
DB.put('aulas_puntos_interes', mid, data)
.then(() => {
toastr.success('Guardado!');
setTimeout(() => {
document.getElementById('actionStatus').style.display = 'none';
setUrlHash('aulas,puntos_interes');
}, SAVE_WAIT);
})
.catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = '1';
document.getElementById('actionStatus').style.display = 'none';
toastr.error('Error al guardar el punto de interés');
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm('¿Quieres borrar este punto de interés?') == true) {
DB.del('aulas_puntos_interes', mid).then(() => {
toastr.error('Borrado!');
setTimeout(() => {
setUrlHash('aulas,puntos_interes');
}, SAVE_WAIT);
});
}
};
},
_resumen_diario: function () {
var data_Comedor = safeuuid();
var data_Tareas = safeuuid();
var data_Diario = safeuuid();
var data_Weather = safeuuid();
if (!checkRole('aulas:resumen_diario')) {
setUrlHash('index');
return;
}
container.innerHTML = html`
<h1>Resumen Diario ${CurrentISODate()}</h1>
<button onclick="print()" class="no_print">Imprimir</button>
<a class="button no_print" href="#aulas">← Volver a Gestión de Aulas</a>
<br /><span
class="btn7"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"
><b>Menú Comedor:</b> <br /><span id="${data_Comedor}">Cargando...</span></span
>
<br /><span
class="btn6"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"
><b>Tareas:</b> <br />
<pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Tareas}">
Cargando...</pre
>
</span>
<br /><span
class="btn5"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"
><b>Informe:</b> <br />
<pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Diario}">
Cargando...</pre
>
</span>
<br /><span
class="btn4"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"
><b>Clima:</b> <br /><img
loading="lazy"
style="padding: 15px; background-color: white; height: 75px;"
id="${data_Weather}"
/></span>
`;
//#region Cargar Clima
// Get location from DB settings.weather_location; if missing ask user and save it
// url format: https://wttr.in/<loc>?F0m
DB.get('settings', 'weather_location').then((loc) => {
if (!loc) {
loc = prompt('Introduce tu ubicación para el clima (ciudad, país):', 'Madrid, Spain');
if (loc) {
DB.put('settings', 'weather_location', loc);
}
}
if (loc) {
document.getElementById(data_Weather).src =
'https://wttr.in/' + encodeURIComponent(loc) + '_IF0m_background=FFFFFF.png';
} else {
document.getElementById(data_Weather).src = 'https://wttr.in/_IF0m_background=FFFFFF.png';
}
});
//#endregion Cargar Clima
//#region Cargar Comedor
DB.get('comedor', CurrentISODate()).then((data) => {
function add_row(data) {
if (!data.Primero) {
var result = 'No hay información del comedor para hoy.';
} else {
var result = data.Primero + "<br>" + data.Segundo + "<br>" + data.Postre;
}
// Display platos
document.getElementById(data_Comedor).innerHTML = result;
}
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'comedor',
CurrentISODate()
);
} else {
add_row(data || {});
}
});
//#endregion Cargar Comedor
//#region Cargar Tareas
DB.get('notas', 'tareas').then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || 'No hay tareas.';
// Display tareas
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(/\n/g, '<br>');
}
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'notas',
'tareas'
);
} else {
add_row(data || {});
}
});
//#endregion Cargar Tareas
//#region Cargar Diario
DB.get('aulas_informes', 'diario-' + CurrentISODate()).then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || 'No hay un diario.';
// Display platos
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(/\n/g, '<br>');
}
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'aulas_informes',
'diario-' + CurrentISODate()
);
} else {
add_row(data || {});
}
});
//#endregion Cargar Diario
},
edit: function (fsection) {
if (!checkRole('aulas')) {
setUrlHash('index');
@@ -678,6 +1181,12 @@ Cargando...</pre
case 'ordenadores':
this._ordenadores();
break;
case 'puntos_interes':
this._puntos_interes();
break;
case 'resumen_diario':
this._resumen_diario();
break;
default:
this.index();
break;
@@ -694,6 +1203,9 @@ Cargando...</pre
case 'ordenadores':
this._ordenadores__edit(item);
break;
case 'puntos_interes':
this._puntos_interes__edit(item);
break;
}
}
},

View File

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

View File

@@ -285,9 +285,9 @@ PAGES.materiales = {
<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>
<option value="Entrada">Entrada - Meter al almacen</option>
<option value="Salida">Salida - Sacar del almacen</option>
<option value="Ajuste">Ajuste - Existencias actuales</option>
</select>
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
@@ -424,7 +424,7 @@ PAGES.materiales = {
var nota = document.getElementById(mov_nota).value || '';
var actual = parseNum(document.getElementById(field_cantidad).value, 0);
if (!Number.isFinite(cantidadMov) || cantidadMov <= 0) {
if ((!Number.isFinite(cantidadMov) || cantidadMov <= 0) && tipo !== 'Ajuste') {
toastr.warning('Indica una cantidad válida para el movimiento');
return;
}

View File

@@ -1,294 +0,0 @@
PERMS['mensajes'] = 'Mensajes';
PERMS['mensajes:edit'] = '&gt; 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(''));
};
}
},
};

View File

@@ -718,111 +718,35 @@ PAGES.pagos = {
var btn_revert = safeuuid();
container.innerHTML = html`
<h1>Transacción <code id="${nameh1}"></code></h1>
${BuildQR('pagos,' + tid, 'Esta Transacción')}
<button id="${btn_volver}">← Volver a Pagos</button>
<button id="${btn_volver2}">← Volver a SuperCafé</button>
<fieldset>
<legend>Detalles de la Transacción</legend>
<h1 class="no_print">Transacción <code id="${nameh1}"></code></h1>
<button class="no_print" id="${btn_volver}">← Volver a Pagos</button>
<button class="no_print" id="${btn_volver2}">← Volver a SuperCafé</button>
<h4>Ticket - ${tid}</h4>
<b>Fecha</b>: <span id="${field_fecha}"></span><br />
<b>Operación</b>: <span id="${field_tipo}"></span> realizado por <span id="${field_persona}"></span><br />
<label>
Ticket/ID<br />
<input
type="text"
id="${field_ticket}"
readonly
style="background: #f0f0f0;"
/><br /><br />
</label>
<div id="${div_persona_destino}" style="display: none;">
<b>Destino</b>: <span id="${field_persona_destino}"></span><br />
</div>
<label>
Fecha y Hora<br />
<input
type="text"
id="${field_fecha}"
readonly
style="background: #f0f0f0;"
/><br /><br />
</label>
<b>Metodo</b>: <span id="${field_metodo}"></span><br />
<label>
Tipo<br />
<input type="text" id="${field_tipo}" readonly style="background: #f0f0f0;" /><br /><br />
</label>
<div id="${div_origen}" style="display: none;">
<b>Origen</b>: <span id="${field_origen}"></span><br />
</div>
<hr />
<span id="${field_notas}"></span><br />
<hr>
<b>Estado</b>: <span id="${field_estado}"></span><br />
<h1 id="${field_monto}" style="color: green; text-align: center;">0.00€</h1>
<hr>
<label>
Monto<br />
<input
type="text"
id="${field_monto}"
readonly
style="background: #f0f0f0; font-size: 24px; font-weight: bold;"
/><br /><br />
</label>
<label>
Monedero (Persona)<br />
<input
type="text"
id="${field_persona}"
readonly
style="background: #f0f0f0;"
/><br /><br />
</label>
<div id="${div_persona_destino}" style="display: none;">
<label>
Monedero Destino<br />
<input
type="text"
id="${field_persona_destino}"
readonly
style="background: #f0f0f0;"
/><br /><br />
</label>
</div>
<label>
Método de Pago<br />
<input
type="text"
id="${field_metodo}"
readonly
style="background: #f0f0f0;"
/><br /><br />
</label>
<label>
Estado<br />
<input
type="text"
id="${field_estado}"
readonly
style="background: #f0f0f0;"
/><br /><br />
</label>
<div id="${div_origen}" style="display: none;">
<label>
Origen<br />
<input
type="text"
id="${field_origen}"
readonly
style="background: #f0f0f0;"
/><br /><br />
</label>
</div>
<label>
Notas<br />
<textarea id="${field_notas}" readonly rows="4" style="background: #f0f0f0;"></textarea
><br /><br />
</label>
</fieldset>
<fieldset style="margin-top: 20px;">
<fieldset style="margin-top: 20px;" class="no_print">
<legend>Acciones</legend>
<button onclick="window.print()" class="btn4" style="font-size: 16px; padding: 10px 20px; margin: 5px;">
🖨️ Imprimir Ticket
</button>
<button
id="${btn_edit}"
class="btn5"
@@ -859,33 +783,33 @@ PAGES.pagos = {
function load_data(data) {
console.log('Transaction data:', data);
document.getElementById(nameh1).innerText = tid;
document.getElementById(field_ticket).value = data.Ticket || tid;
//document.getElementById(field_ticket).innerText = data.Ticket || tid;
var fecha = data.Fecha || '';
if (fecha) {
var d = new Date(fecha);
document.getElementById(field_fecha).value = d.toLocaleString('es-ES');
document.getElementById(field_fecha).innerText = d.toLocaleString('es-ES');
}
document.getElementById(field_tipo).value = data.Tipo || '';
document.getElementById(field_monto).value = (data.Monto || 0).toFixed(2) + '€';
document.getElementById(field_tipo).innerText = data.Tipo || '';
document.getElementById(field_monto).innerText = (data.Monto || 0).toFixed(2) + '€';
var persona = SC_Personas[data.Persona] || {};
document.getElementById(field_persona).value = persona.Nombre || data.Persona || '';
document.getElementById(field_persona).innerText = persona.Nombre || data.Persona || '';
if (data.PersonaDestino) {
var personaDestino = SC_Personas[data.PersonaDestino] || {};
document.getElementById(field_persona_destino).value =
document.getElementById(field_persona_destino).innerText =
personaDestino.Nombre || data.PersonaDestino || '';
document.getElementById(div_persona_destino).style.display = 'block';
}
document.getElementById(field_metodo).value = data.Metodo || '';
document.getElementById(field_estado).value = data.Estado || '';
document.getElementById(field_notas).value = data.Notas || '';
document.getElementById(field_metodo).innerText = data.Metodo || '';
document.getElementById(field_estado).innerText = data.Estado || '';
document.getElementById(field_notas).innerText = data.Notas || '';
if (data.Origen) {
document.getElementById(field_origen).value =
document.getElementById(field_origen).innerText =
data.Origen + (data.OrigenID ? ' (' + data.OrigenID + ')' : '');
document.getElementById(div_origen).style.display = 'block';
}

View File

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