11 Commits

Author SHA1 Message Date
Naiel
c940c846b8 Mejorar la presentación de la transacción en la página de pagos y optimizar la carga de datos 2026-03-04 23:02:26 +00:00
Naiel
7822e46b61 Añadir funcionalidad para gestionar puntos de interés en Gest-Aula 2026-03-04 14:45:04 +00:00
Naiel
a39ecca990 Mejoras generales, rediseño de Gest-Aula, borrado de modulos sin usar y Mensajes, movido el resumen diario a gest-aula. 2026-03-04 14:33:28 +00:00
Naiel
80e9262bcb fix logic 2026-03-03 11:33:23 +01:00
Naiel
cd456ab9f1 fix 2026-03-03 11:29:38 +01:00
Naiel
c0c40ecd99 fix 2026-03-03 11:24:19 +01:00
Naiel
98c6ba39f3 fix 2026-03-03 11:20:57 +01:00
Naiel
03f52c8a92 fix 2026-03-03 11:06:30 +01:00
Naiel
f655a736b3 fix 2026-03-03 10:59:02 +01:00
Naiel
89a68f27da fix 2026-03-03 10:50:05 +01:00
Naiel
4d322e5696 fix 2026-03-03 10:36:42 +01:00
11 changed files with 734 additions and 997 deletions

View File

@@ -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,

View File

@@ -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:

View File

@@ -7,7 +7,7 @@ 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 base64
@@ -84,6 +84,7 @@ 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_decrypt(input_value: Any, secret: str) -> Any:
""" """
Compatible with JS TS_decrypt behavior: Compatible with JS TS_decrypt behavior:
@@ -130,7 +131,6 @@ def ts_decrypt(input_value: Any, secret: str) -> Any:
except Exception: except Exception:
return input_value return input_value
@dataclass @dataclass
class TeleSecDoc: class TeleSecDoc:
id: str id: str
@@ -372,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:
@@ -415,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")

View File

@@ -949,9 +949,11 @@ function TS_decrypt(input, secret, callback, table, id) {
} }
// Encrypted format marker: RSA{<ciphertext>} where <ciphertext> is CryptoJS AES output // 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 {

View File

@@ -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>

View File

@@ -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: '&copy; OpenStreetMap',
}).addTo(map);
layer = L.layerGroup().addTo(map);
EventListeners.DB.push(
DB.map('aulas_puntos_interes', (raw, key) => {
if (raw === null) {
removeMarker(key);
return;
}
PAGES.aulas
.__decryptIfNeeded('aulas_puntos_interes', key, raw)
.then((data) => {
data = data || {};
data._key = key;
updateMarker(data);
})
.catch((e) => {
console.warn('Error cargando punto de interés para mapa', e);
});
})
);
} catch (e) {
console.warn('Leaflet no disponible', e);
toastr.error('No se pudo cargar el mapa');
}
})();
},
_puntos_interes__edit: function (mid) {
var field_nombre = safeuuid();
var field_tipo = safeuuid();
var field_direccion = safeuuid();
var field_descripcion = safeuuid();
var field_lat = safeuuid();
var field_lng = safeuuid();
var btn_gps = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var map_id = safeuuid();
container.innerHTML = html`
<a class="button" href="#aulas,puntos_interes">← Volver a puntos de interés</a>
<h1>Punto de interés <code>${mid}</code></h1>
<fieldset style="float: none; width: calc(100% - 40px); max-width: none;">
<legend>Datos</legend>
<label>Nombre<br /><input type="text" id="${field_nombre}" /></label><br /><br />
<label>Tipo (tienda, bar, entidad...)<br /><input type="text" id="${field_tipo}" /></label><br /><br />
<label>Dirección<br /><input type="text" id="${field_direccion}" style="width: calc(100% - 20px);" /></label><br /><br />
<label>Descripción<br /><textarea id="${field_descripcion}" style="width: 100%; height: 120px;"></textarea></label><br /><br />
<div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: end;">
<label>Latitud<br /><input type="text" id="${field_lat}" placeholder="40.4168" /></label>
<label>Longitud<br /><input type="text" id="${field_lng}" placeholder="-3.7038" /></label>
<button class="btn5" id="${btn_gps}">Usar GPS</button>
</div>
<br />
<div id="${map_id}" style="height: 360px; border: 2px solid black; border-radius: 8px;"></div>
<hr />
<button class="saveico" id="${btn_guardar}">
<img src="static/floppy_disk_green.png" />
<br>Guardar
</button>
<button class="delico" id="${btn_borrar}">
<img src="static/garbage.png" />
<br>Borrar
</button>
</fieldset>
`;
var map = null;
var marker = null;
function parseCoord(value) {
if (value === null || value === undefined || value === '') return null;
var parsed = parseFloat(String(value).replace(',', '.'));
return isNaN(parsed) ? null : parsed;
}
function setCoordInputs(lat, lng) {
document.getElementById(field_lat).value =
lat === null || lat === undefined ? '' : Number(lat).toFixed(6);
document.getElementById(field_lng).value =
lng === null || lng === undefined ? '' : Number(lng).toFixed(6);
}
function refreshMarker(centerMap = false) {
if (!map) return;
var lat = parseCoord(document.getElementById(field_lat).value);
var lng = parseCoord(document.getElementById(field_lng).value);
if (lat === null || lng === null) {
if (marker) {
map.removeLayer(marker);
marker = null;
}
return;
}
if (!marker) {
marker = L.marker([lat, lng], { draggable: true }).addTo(map);
marker.on('dragend', (ev) => {
var ll = ev.target.getLatLng();
setCoordInputs(ll.lat, ll.lng);
});
} else {
marker.setLatLng([lat, lng]);
}
if (centerMap) {
map.setView([lat, lng], 17);
}
}
(async () => {
try {
await PAGES.aulas.__ensureLeaflet();
map = L.map(map_id).setView([40.4168, -3.7038], 6);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; OpenStreetMap',
}).addTo(map);
map.on('click', (ev) => {
setCoordInputs(ev.latlng.lat, ev.latlng.lng);
refreshMarker(false);
});
var raw = await DB.get('aulas_puntos_interes', mid);
var data = await PAGES.aulas.__decryptIfNeeded('aulas_puntos_interes', mid, raw);
data = data || {};
document.getElementById(field_nombre).value = data.Nombre || '';
document.getElementById(field_tipo).value = data.Tipo || '';
document.getElementById(field_direccion).value = data.Direccion || '';
document.getElementById(field_descripcion).value = data.Descripcion || '';
setCoordInputs(parseCoord(data.Latitud), parseCoord(data.Longitud));
refreshMarker(true);
} catch (e) {
console.warn('Error iniciando mapa de punto de interés', e);
toastr.error('No se pudo cargar el mapa del punto de interés');
}
})();
document.getElementById(field_lat).addEventListener('input', () => refreshMarker(false));
document.getElementById(field_lng).addEventListener('input', () => refreshMarker(false));
document.getElementById(btn_gps).onclick = (ev) => {
ev.preventDefault();
if (!navigator.geolocation) {
toastr.error('Tu navegador no soporta geolocalización');
return false;
}
navigator.geolocation.getCurrentPosition(
(pos) => {
setCoordInputs(pos.coords.latitude, pos.coords.longitude);
refreshMarker(true);
toastr.success('Ubicación GPS capturada');
},
(err) => {
console.warn('Error GPS', err);
toastr.error('No se pudo obtener tu ubicación GPS');
},
{ enableHighAccuracy: true, timeout: 10000 }
);
return false;
};
document.getElementById(btn_guardar).onclick = () => {
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
var lat = parseCoord(document.getElementById(field_lat).value);
var lng = parseCoord(document.getElementById(field_lng).value);
var hasLat = document.getElementById(field_lat).value.trim() !== '';
var hasLng = document.getElementById(field_lng).value.trim() !== '';
if ((hasLat && lat === null) || (hasLng && lng === null) || (hasLat !== hasLng)) {
toastr.error('Introduce latitud y longitud válidas');
return;
}
guardarBtn.disabled = true;
guardarBtn.style.opacity = '0.5';
var data = {
Nombre: document.getElementById(field_nombre).value,
Tipo: document.getElementById(field_tipo).value,
Direccion: document.getElementById(field_direccion).value,
Descripcion: document.getElementById(field_descripcion).value,
Latitud: lat === null ? '' : Number(lat).toFixed(6),
Longitud: lng === null ? '' : Number(lng).toFixed(6),
UpdatedAt: new Date().toISOString(),
Autor: SUB_LOGGED_IN_ID || '',
};
document.getElementById('actionStatus').style.display = 'block';
DB.put('aulas_puntos_interes', mid, data)
.then(() => {
toastr.success('Guardado!');
setTimeout(() => {
document.getElementById('actionStatus').style.display = 'none';
setUrlHash('aulas,puntos_interes');
}, SAVE_WAIT);
})
.catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = '1';
document.getElementById('actionStatus').style.display = 'none';
toastr.error('Error al guardar el punto de interés');
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm('¿Quieres borrar este punto de interés?') == true) {
DB.del('aulas_puntos_interes', mid).then(() => {
toastr.error('Borrado!');
setTimeout(() => {
setUrlHash('aulas,puntos_interes');
}, SAVE_WAIT);
});
}
};
},
_resumen_diario: function () {
var data_Comedor = safeuuid();
var data_Tareas = safeuuid();
var data_Diario = safeuuid();
var data_Weather = safeuuid();
if (!checkRole('aulas:resumen_diario')) {
setUrlHash('index');
return;
}
container.innerHTML = html`
<h1>Resumen Diario ${CurrentISODate()}</h1>
<button onclick="print()" class="no_print">Imprimir</button>
<a class="button no_print" href="#aulas">← Volver a Gestión de Aulas</a>
<br /><span
class="btn7"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"
><b>Menú Comedor:</b> <br /><span id="${data_Comedor}">Cargando...</span></span
>
<br /><span
class="btn6"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"
><b>Tareas:</b> <br />
<pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Tareas}">
Cargando...</pre
>
</span>
<br /><span
class="btn5"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"
><b>Informe:</b> <br />
<pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Diario}">
Cargando...</pre
>
</span>
<br /><span
class="btn4"
style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"
><b>Clima:</b> <br /><img
loading="lazy"
style="padding: 15px; background-color: white; height: 75px;"
id="${data_Weather}"
/></span>
`;
//#region Cargar Clima
// Get location from DB settings.weather_location; if missing ask user and save it
// url format: https://wttr.in/<loc>?F0m
DB.get('settings', 'weather_location').then((loc) => {
if (!loc) {
loc = prompt('Introduce tu ubicación para el clima (ciudad, país):', 'Madrid, Spain');
if (loc) {
DB.put('settings', 'weather_location', loc);
}
}
if (loc) {
document.getElementById(data_Weather).src =
'https://wttr.in/' + encodeURIComponent(loc) + '_IF0m_background=FFFFFF.png';
} else {
document.getElementById(data_Weather).src = 'https://wttr.in/_IF0m_background=FFFFFF.png';
}
});
//#endregion Cargar Clima
//#region Cargar Comedor
DB.get('comedor', CurrentISODate()).then((data) => {
function add_row(data) {
if (!data.Primero) {
var result = 'No hay información del comedor para hoy.';
} else {
var result = data.Primero + "<br>" + data.Segundo + "<br>" + data.Postre;
}
// Display platos
document.getElementById(data_Comedor).innerHTML = result;
}
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'comedor',
CurrentISODate()
);
} else {
add_row(data || {});
}
});
//#endregion Cargar Comedor
//#region Cargar Tareas
DB.get('notas', 'tareas').then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || 'No hay tareas.';
// Display tareas
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(/\n/g, '<br>');
}
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'notas',
'tareas'
);
} else {
add_row(data || {});
}
});
//#endregion Cargar Tareas
//#region Cargar Diario
DB.get('aulas_informes', 'diario-' + CurrentISODate()).then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || 'No hay un diario.';
// Display platos
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(/\n/g, '<br>');
}
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
add_row(data || {});
},
'aulas_informes',
'diario-' + CurrentISODate()
);
} else {
add_row(data || {});
}
});
//#endregion Cargar Diario
},
edit: function (fsection) { 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;
} }
} }
}, },

View File

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

View File

@@ -285,9 +285,9 @@ PAGES.materiales = {
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;"> <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;
} }

View File

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

View File

@@ -718,111 +718,35 @@ PAGES.pagos = {
var btn_revert = safeuuid(); 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';
} }

View File

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