Compare commits
15 Commits
v2026.03.1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c940c846b8 | ||
|
|
7822e46b61 | ||
|
|
a39ecca990 | ||
|
|
80e9262bcb | ||
|
|
cd456ab9f1 | ||
|
|
c0c40ecd99 | ||
|
|
98c6ba39f3 | ||
|
|
03f52c8a92 | ||
|
|
f655a736b3 | ||
|
|
89a68f27da | ||
|
|
4d322e5696 | ||
|
|
0138e0ca69 | ||
|
|
3e8542c9de | ||
|
|
90df81d308 | ||
|
|
53941da35c |
6
.github/workflows/windows-agent-release.yml
vendored
6
.github/workflows/windows-agent-release.yml
vendored
@@ -31,16 +31,16 @@ jobs:
|
|||||||
- name: Build hidden EXE with PyInstaller
|
- name: Build hidden EXE with PyInstaller
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
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
|
- name: Upload workflow artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: telesec-windows-agent
|
name: telesec-windows-agent
|
||||||
path: dist/telesec-windows-agent.exe
|
path: python_sdk/dist/telesec-windows-agent.exe
|
||||||
|
|
||||||
- name: Upload asset to GitHub Release
|
- name: Upload asset to GitHub Release
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: dist/telesec-windows-agent.exe
|
files: python_sdk/dist/telesec-windows-agent.exe
|
||||||
|
|||||||
@@ -65,9 +65,11 @@ a:hover {
|
|||||||
@media print {
|
@media print {
|
||||||
.supermesh-indicator,
|
.supermesh-indicator,
|
||||||
.no_print,
|
.no_print,
|
||||||
.no_print * {
|
.no_print *,
|
||||||
|
.saveico, .delico, .opicon {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
main {padding: 0;}
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
from .telesec_couchdb import TeleSecCouchDB, ts_encrypt, ts_decrypt
|
|
||||||
|
|
||||||
__all__ = ["TeleSecCouchDB", "ts_encrypt", "ts_decrypt"]
|
|
||||||
@@ -76,52 +76,32 @@ def ts_encrypt(input_value: Any, secret: str) -> str:
|
|||||||
return f"RSA{{{b64}}}"
|
return f"RSA{{{b64}}}"
|
||||||
|
|
||||||
|
|
||||||
def ts_decrypt(input_value: Any, secret: str) -> Any:
|
def ts_encrypt(input_value: Any, secret: str) -> str:
|
||||||
"""
|
|
||||||
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):
|
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("}")
|
payload_bytes = payload.encode("utf-8")
|
||||||
if is_wrapped:
|
salt = os.urandom(8)
|
||||||
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__"):
|
# OpenSSL EVP_BytesToKey (MD5)
|
||||||
raise TeleSecCryptoError("Unsupported encrypted payload format")
|
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]
|
key = salted[:32]
|
||||||
ciphertext = raw[16:]
|
iv = salted[32:48]
|
||||||
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:
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||||
text = decrypted.decode("utf-8")
|
encrypted = cipher.encrypt(_pkcs7_pad(payload_bytes, 16))
|
||||||
except UnicodeDecodeError:
|
|
||||||
text = decrypted.decode("latin-1")
|
|
||||||
|
|
||||||
try:
|
openssl_blob = b"Salted__" + salt + encrypted
|
||||||
return json.loads(text)
|
b64 = base64.b64encode(openssl_blob).decode("utf-8")
|
||||||
except Exception:
|
|
||||||
return text
|
|
||||||
|
|
||||||
try:
|
|
||||||
return json.loads(input_value)
|
|
||||||
except Exception:
|
|
||||||
return input_value
|
|
||||||
|
|
||||||
|
return f"RSA{{{b64}}}"
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TeleSecDoc:
|
class TeleSecDoc:
|
||||||
|
|||||||
@@ -7,16 +7,299 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional, List
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
|
import base64
|
||||||
|
import email.utils
|
||||||
|
import hashlib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
try:
|
import requests
|
||||||
from .telesec_couchdb import TeleSecCouchDB, TeleSecCouchDBError, ts_decrypt
|
from Crypto.Cipher import AES
|
||||||
except ImportError:
|
|
||||||
from telesec_couchdb import TeleSecCouchDB, TeleSecCouchDBError, ts_decrypt
|
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
def utcnow_iso() -> str:
|
||||||
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||||
|
|
||||||
@@ -89,7 +372,7 @@ def should_shutdown(data: Dict[str, Any], server_now: datetime) -> bool:
|
|||||||
target = parse_iso(str(data.get("ShutdownBeforeDate", "") or ""))
|
target = parse_iso(str(data.get("ShutdownBeforeDate", "") or ""))
|
||||||
if not target:
|
if not target:
|
||||||
return False
|
return False
|
||||||
return server_now >= target
|
return server_now <= target
|
||||||
|
|
||||||
|
|
||||||
def execute_shutdown(dry_run: bool = False) -> None:
|
def execute_shutdown(dry_run: bool = False) -> None:
|
||||||
@@ -132,7 +415,7 @@ def run_once(client: TeleSecCouchDB, machine_id: str, dry_run: bool = False) ->
|
|||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description="TeleSec Windows Agent")
|
parser = argparse.ArgumentParser(description="TeleSec Windows Agent")
|
||||||
parser.add_argument("--server", default="", help="CouchDB server URL, ej. https://couch.example")
|
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("--db", default="", help="Database name")
|
||||||
parser.add_argument("--user", default="", help="CouchDB username")
|
parser.add_argument("--user", default="", help="CouchDB username")
|
||||||
parser.add_argument("--password", default="", help="CouchDB password")
|
parser.add_argument("--password", default="", help="CouchDB password")
|
||||||
parser.add_argument("--secret", default="", help="TeleSec secret para cifrado")
|
parser.add_argument("--secret", default="", help="TeleSec secret para cifrado")
|
||||||
|
|||||||
@@ -949,9 +949,11 @@ function TS_decrypt(input, secret, callback, table, id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encrypted format marker: RSA{<ciphertext>} where <ciphertext> is CryptoJS AES output
|
// Encrypted format marker: RSA{<ciphertext>} where <ciphertext> is CryptoJS AES output
|
||||||
|
// console.debug(input);
|
||||||
if (input.startsWith('RSA{') && input.endsWith('}') && typeof CryptoJS !== 'undefined') {
|
if (input.startsWith('RSA{') && input.endsWith('}') && typeof CryptoJS !== 'undefined') {
|
||||||
try {
|
try {
|
||||||
var data = input.slice(4, -1);
|
var data = input.slice(4, -1);
|
||||||
|
// console.debug("TS_decrypt secret:", ">" + secret + "<", typeof secret, secret?.length);
|
||||||
var words = CryptoJS.AES.decrypt(data, secret);
|
var words = CryptoJS.AES.decrypt(data, secret);
|
||||||
var decryptedUtf8 = null;
|
var decryptedUtf8 = null;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -85,15 +85,11 @@
|
|||||||
<script src="page/dataman.js"></script>
|
<script src="page/dataman.js"></script>
|
||||||
<script src="page/aulas.js"></script>
|
<script src="page/aulas.js"></script>
|
||||||
<script src="page/materiales.js"></script>
|
<script src="page/materiales.js"></script>
|
||||||
<script src="page/resumen_diario.js"></script>
|
|
||||||
<script src="page/personas.js"></script>
|
<script src="page/personas.js"></script>
|
||||||
<script src="page/supercafe.js"></script>
|
<script src="page/supercafe.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/panel.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>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
PERMS['aulas'] = 'Aulas (Solo docentes!)';
|
PERMS['aulas'] = 'Aulas (Solo docentes!)';
|
||||||
|
PERMS['aulas:resumen_diario'] = '> Resumen diario';
|
||||||
|
PERMS['aulas:puntos_interes'] = '> Puntos de interés';
|
||||||
PAGES.aulas = {
|
PAGES.aulas = {
|
||||||
//navcss: "btn1",
|
//navcss: "btn1",
|
||||||
Title: 'Gest-Aula',
|
Title: 'Gest-Aula',
|
||||||
@@ -13,168 +15,102 @@ PAGES.aulas = {
|
|||||||
var data_Tareas = safeuuid();
|
var data_Tareas = safeuuid();
|
||||||
var data_Diario = safeuuid();
|
var data_Diario = safeuuid();
|
||||||
var data_Weather = 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`
|
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>Atajos de hoy</legend>
|
||||||
<a class="button" style="font-size: 25px;" href="#notas,inicio_dia"
|
<a class="button" id="${link_alertas}" href="#notas,alertas">
|
||||||
>Como iniciar el día</a
|
Alertas
|
||||||
>
|
</a>
|
||||||
<a class="button" style="font-size: 25px;" href="#notas,realizacion_cafe"
|
<a class="button btn2" href="#aulas,solicitudes,${safeuuid('')}">
|
||||||
>Como realizar el café</a
|
Solicitar material
|
||||||
>
|
</a>
|
||||||
<a class="button" style="font-size: 25px;" href="#notas,fin_dia">Como acabar el día</a>
|
<a class="button" id="${link_diario}" href="#aulas,informes,diario-${CurrentISODate()}">
|
||||||
<a class="button" style="font-size: 25px;" href="#notas,horario">Horario</a>
|
Informe
|
||||||
<a class="button" style="font-size: 25px;" href="#notas,tareas">Tareas</a>
|
</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>
|
||||||
<fieldset style="float: left;">
|
<fieldset style="float: left;">
|
||||||
<legend>Acciones</legend>
|
<legend>Acciones</legend>
|
||||||
<a class="button" style="font-size: 25px;" href="#aulas,solicitudes"
|
<a class="button" style="font-size: 25px;" href="#aulas,solicitudes">
|
||||||
><img src="${PAGES.materiales.icon}" height="20" /> Solicitudes de material</a
|
Solicitudes de materiales
|
||||||
>
|
</a>
|
||||||
<a
|
<a class="button" style="font-size: 25px;" href="#aulas,informes">
|
||||||
class="button"
|
Informes
|
||||||
style="font-size: 25px;"
|
</a>
|
||||||
href="#aulas,informes,diario-${CurrentISODate()}"
|
<a class="button btn8" style="font-size: 25px;" href="#aulas,puntos_interes">
|
||||||
>Diario de hoy</a
|
Puntos de interés
|
||||||
>
|
</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>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
//#region Cargar Clima
|
//#region Contar alertas activas y mostrarlas en el botón
|
||||||
// Get location from DB settings.weather_location; if missing ask user and save it
|
DB.get('notas', 'alertas')
|
||||||
// url format: https://wttr.in/<loc>?F0m
|
.then((res) => TS_decrypt(res, SECRET, (data) => {
|
||||||
DB.get('settings', 'weather_location').then((loc) => {
|
var count = 0;
|
||||||
if (!loc) {
|
// Sumar el total de alertas activas, cada linea de "Contenido"
|
||||||
loc = prompt('Introduce tu ubicación para el clima (ciudad, país):', 'Madrid, Spain');
|
// es una alerta, aunque podrían hacerse varias por nota.
|
||||||
if (loc) {
|
// Ignora lineas que no empiezen por > (por si el profesor escribe algo que no es una alerta)
|
||||||
DB.put('settings', 'weather_location', loc);
|
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) {
|
.catch((e) => {
|
||||||
document.getElementById(data_Weather).src =
|
console.warn('Error contando alertas activas', e);
|
||||||
'https://wttr.in/' + encodeURIComponent(loc) + '_IF0m_background=FFFFFF.png';
|
});
|
||||||
|
//#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 {
|
} 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
|
//#endregion Comprobar actividades
|
||||||
//#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
|
|
||||||
},
|
},
|
||||||
_solicitudes: function () {
|
_solicitudes: function () {
|
||||||
const tablebody = safeuuid();
|
const tablebody = safeuuid();
|
||||||
@@ -190,7 +126,7 @@ Cargando...</pre
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: 'Solicitante',
|
key: 'Solicitante',
|
||||||
type: 'persona',
|
type: 'persona-nombre',
|
||||||
default: '',
|
default: '',
|
||||||
label: 'Solicitante',
|
label: 'Solicitante',
|
||||||
},
|
},
|
||||||
@@ -233,8 +169,14 @@ Cargando...</pre
|
|||||||
><br /><br />
|
><br /><br />
|
||||||
</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>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -311,7 +253,7 @@ Cargando...</pre
|
|||||||
<div
|
<div
|
||||||
style="display: inline-block; border: 2px solid black; padding: 5px; border-radius: 5px;"
|
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()}" />
|
<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>
|
</div>
|
||||||
@@ -324,7 +266,7 @@ Cargando...</pre
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: 'Autor',
|
key: 'Autor',
|
||||||
type: 'persona',
|
type: 'persona-nombre',
|
||||||
default: '',
|
default: '',
|
||||||
label: 'Autor',
|
label: 'Autor',
|
||||||
},
|
},
|
||||||
@@ -367,7 +309,10 @@ Cargando...</pre
|
|||||||
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 = '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`
|
container.innerHTML = html`
|
||||||
<a class="button" href="#aulas,informes">← Volver a informes</a>
|
<a class="button" href="#aulas,informes">← Volver a informes</a>
|
||||||
@@ -388,8 +333,14 @@ Cargando...</pre
|
|||||||
><br /><br />
|
><br /><br />
|
||||||
</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>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
(async () => {
|
(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 () {
|
__getServerNow: async function () {
|
||||||
try {
|
try {
|
||||||
var couchUrl = (localStorage.getItem('TELESEC_COUCH_URL') || '').replace(/\/$/, '');
|
var couchUrl = (localStorage.getItem('TELESEC_COUCH_URL') || '').replace(/\/$/, '');
|
||||||
@@ -659,6 +658,510 @@ Cargando...</pre
|
|||||||
await loadData();
|
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: '© 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: '© 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) {
|
edit: function (fsection) {
|
||||||
if (!checkRole('aulas')) {
|
if (!checkRole('aulas')) {
|
||||||
setUrlHash('index');
|
setUrlHash('index');
|
||||||
@@ -678,6 +1181,12 @@ Cargando...</pre
|
|||||||
case 'ordenadores':
|
case 'ordenadores':
|
||||||
this._ordenadores();
|
this._ordenadores();
|
||||||
break;
|
break;
|
||||||
|
case 'puntos_interes':
|
||||||
|
this._puntos_interes();
|
||||||
|
break;
|
||||||
|
case 'resumen_diario':
|
||||||
|
this._resumen_diario();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this.index();
|
this.index();
|
||||||
break;
|
break;
|
||||||
@@ -694,6 +1203,9 @@ Cargando...</pre
|
|||||||
case 'ordenadores':
|
case 'ordenadores':
|
||||||
this._ordenadores__edit(item);
|
this._ordenadores__edit(item);
|
||||||
break;
|
break;
|
||||||
|
case 'puntos_interes':
|
||||||
|
this._puntos_interes__edit(item);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,247 +0,0 @@
|
|||||||
PERMS['avisos'] = 'Avisos';
|
|
||||||
PERMS['avisos:edit'] = '> 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(''));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -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;">
|
<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>
|
<label for="${mov_tipo}">Tipo</label>
|
||||||
<select id="${mov_tipo}" style="flex: 1;">
|
<select id="${mov_tipo}" style="flex: 1;">
|
||||||
<option value="Entrada">Entrada</option>
|
<option value="Entrada">Entrada - Meter al almacen</option>
|
||||||
<option value="Salida">Salida</option>
|
<option value="Salida">Salida - Sacar del almacen</option>
|
||||||
<option value="Ajuste">Ajuste</option>
|
<option value="Ajuste">Ajuste - Existencias actuales</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
|
<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 nota = document.getElementById(mov_nota).value || '';
|
||||||
var actual = parseNum(document.getElementById(field_cantidad).value, 0);
|
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');
|
toastr.warning('Indica una cantidad válida para el movimiento');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,294 +0,0 @@
|
|||||||
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(''));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -718,111 +718,35 @@ PAGES.pagos = {
|
|||||||
var btn_revert = safeuuid();
|
var btn_revert = safeuuid();
|
||||||
|
|
||||||
container.innerHTML = html`
|
container.innerHTML = html`
|
||||||
<h1>Transacción <code id="${nameh1}"></code></h1>
|
<h1 class="no_print">Transacción <code id="${nameh1}"></code></h1>
|
||||||
${BuildQR('pagos,' + tid, 'Esta Transacción')}
|
<button class="no_print" id="${btn_volver}">← Volver a Pagos</button>
|
||||||
<button id="${btn_volver}">← Volver a Pagos</button>
|
<button class="no_print" id="${btn_volver2}">← Volver a SuperCafé</button>
|
||||||
<button id="${btn_volver2}">← Volver a SuperCafé</button>
|
|
||||||
<fieldset>
|
<h4>Ticket - ${tid}</h4>
|
||||||
<legend>Detalles de la Transacción</legend>
|
<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>
|
<div id="${div_persona_destino}" style="display: none;">
|
||||||
Ticket/ID<br />
|
<b>Destino</b>: <span id="${field_persona_destino}"></span><br />
|
||||||
<input
|
</div>
|
||||||
type="text"
|
|
||||||
id="${field_ticket}"
|
|
||||||
readonly
|
|
||||||
style="background: #f0f0f0;"
|
|
||||||
/><br /><br />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
<b>Metodo</b>: <span id="${field_metodo}"></span><br />
|
||||||
Fecha y Hora<br />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="${field_fecha}"
|
|
||||||
readonly
|
|
||||||
style="background: #f0f0f0;"
|
|
||||||
/><br /><br />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
<div id="${div_origen}" style="display: none;">
|
||||||
Tipo<br />
|
<b>Origen</b>: <span id="${field_origen}"></span><br />
|
||||||
<input type="text" id="${field_tipo}" readonly style="background: #f0f0f0;" /><br /><br />
|
</div>
|
||||||
</label>
|
<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>
|
<fieldset style="margin-top: 20px;" class="no_print">
|
||||||
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;">
|
|
||||||
<legend>Acciones</legend>
|
<legend>Acciones</legend>
|
||||||
|
<button onclick="window.print()" class="btn4" style="font-size: 16px; padding: 10px 20px; margin: 5px;">
|
||||||
|
🖨️ Imprimir Ticket
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
id="${btn_edit}"
|
id="${btn_edit}"
|
||||||
class="btn5"
|
class="btn5"
|
||||||
@@ -859,33 +783,33 @@ PAGES.pagos = {
|
|||||||
function load_data(data) {
|
function load_data(data) {
|
||||||
console.log('Transaction data:', data);
|
console.log('Transaction data:', data);
|
||||||
document.getElementById(nameh1).innerText = tid;
|
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 || '';
|
var fecha = data.Fecha || '';
|
||||||
if (fecha) {
|
if (fecha) {
|
||||||
var d = new Date(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_tipo).innerText = data.Tipo || '';
|
||||||
document.getElementById(field_monto).value = (data.Monto || 0).toFixed(2) + '€';
|
document.getElementById(field_monto).innerText = (data.Monto || 0).toFixed(2) + '€';
|
||||||
|
|
||||||
var persona = SC_Personas[data.Persona] || {};
|
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) {
|
if (data.PersonaDestino) {
|
||||||
var personaDestino = SC_Personas[data.PersonaDestino] || {};
|
var personaDestino = SC_Personas[data.PersonaDestino] || {};
|
||||||
document.getElementById(field_persona_destino).value =
|
document.getElementById(field_persona_destino).innerText =
|
||||||
personaDestino.Nombre || data.PersonaDestino || '';
|
personaDestino.Nombre || data.PersonaDestino || '';
|
||||||
document.getElementById(div_persona_destino).style.display = 'block';
|
document.getElementById(div_persona_destino).style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById(field_metodo).value = data.Metodo || '';
|
document.getElementById(field_metodo).innerText = data.Metodo || '';
|
||||||
document.getElementById(field_estado).value = data.Estado || '';
|
document.getElementById(field_estado).innerText = data.Estado || '';
|
||||||
document.getElementById(field_notas).value = data.Notas || '';
|
document.getElementById(field_notas).innerText = data.Notas || '';
|
||||||
|
|
||||||
if (data.Origen) {
|
if (data.Origen) {
|
||||||
document.getElementById(field_origen).value =
|
document.getElementById(field_origen).innerText =
|
||||||
data.Origen + (data.OrigenID ? ' (' + data.OrigenID + ')' : '');
|
data.Origen + (data.OrigenID ? ' (' + data.OrigenID + ')' : '');
|
||||||
document.getElementById(div_origen).style.display = 'block';
|
document.getElementById(div_origen).style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user