From 543d1c3202eff862f1c6dc88480665c7b73f10d6 Mon Sep 17 00:00:00 2001 From: Naiel <109038805+naielv@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:37:08 +0000 Subject: [PATCH] feat: Add panel page with daily quiz and update functionality - Introduced a new panel page that includes a daily quiz based on the menu and tasks for the day. - Implemented functions to fetch and decrypt daily data, build quiz questions, and render the quiz interface. - Added a button to refresh the application, clearing the cache and updating the service worker. - Enhanced service worker to manage application version checks and handle CouchDB URL prefix. - Created a version.json file to manage application versioning. --- build.py | 3 +- src/index.html | 2 +- src/page/cajas.js | 950 ---------------------------------------------- src/page/index.js | 2 + src/page/panel.js | 357 +++++++++++++++++ src/pwa.js | 166 +++++++- src/sw.js | 32 +- src/version.json | 3 + 8 files changed, 554 insertions(+), 961 deletions(-) delete mode 100644 src/page/cajas.js create mode 100644 src/page/panel.js create mode 100644 src/version.json diff --git a/build.py b/build.py index 75ab305..31a66aa 100644 --- a/build.py +++ b/build.py @@ -2,6 +2,7 @@ import json import os import shutil import sys +import time def get_all_files(directory): files = [] @@ -14,7 +15,7 @@ def get_all_files(directory): return files PREFETCH = "" -VERSIONCO = "2026-02" +VERSIONCO = "2026-02-23_" + time.strftime("%Y%m%d%H%M%S") HANDLEPARSE = get_all_files("src") TITLE = os.environ.get("TELESEC_TITLE", "TeleSec") HOSTER = os.environ.get("TELESEC_HOSTER", "EuskadiTech") diff --git a/src/index.html b/src/index.html index 59e2f53..3c81648 100644 --- a/src/index.html +++ b/src/index.html @@ -86,10 +86,10 @@ + - \ No newline at end of file diff --git a/src/page/cajas.js b/src/page/cajas.js deleted file mode 100644 index df17c33..0000000 --- a/src/page/cajas.js +++ /dev/null @@ -1,950 +0,0 @@ -PERMS['cajas'] = 'Cajas'; -PERMS['cajas:edit'] = '> Editar'; -PAGES.cajas = { - navcss: 'btn8', - icon: 'static/appico/piggy_bank.png', - AccessControl: true, - Title: 'Cajas', - - // View/edit a specific transaction (movimiento) - movimiento: function (cajaId, movimientoId) { - if (!checkRole('cajas')) { - setUrlHash('cajas'); - return; - } - - var field_fecha = safeuuid(); - var field_tipo = safeuuid(); - var field_monto = safeuuid(); - var field_persona = safeuuid(); - var field_notas = safeuuid(); - var field_foto = safeuuid(); - var render_foto = safeuuid(); - var btn_volver = safeuuid(); - var btn_borrar = safeuuid(); - var btn_editar = safeuuid(); - var btn_guardar = safeuuid(); - var btn_cancelar = safeuuid(); - var div_buttons = safeuuid(); - var isEditMode = false; - - container.innerHTML = html` -

Movimiento de Caja

-
- - - - - - -
-
- - - - - -
-
- `; - - // Load transaction data - var movimientoData = null; - var resized = ''; - - DB.get('cajas_movimientos', movimientoId).then((data) => { - function load_data(data) { - if (!data) return; - - movimientoData = data; - - // Format datetime for datetime-local input - var fechaValue = data['Fecha'] || ''; - if (fechaValue) { - // Convert ISO string to datetime-local format (YYYY-MM-DDTHH:mm) - fechaValue = fechaValue.substring(0, 16); - } - - document.getElementById(field_fecha).value = fechaValue; - document.getElementById(field_tipo).value = data['Tipo'] || ''; - document.getElementById(field_monto).value = data['Monto'] || 0; - - // Get persona name - var personaId = data['Persona'] || ''; - var personaName = personaId; - if (SC_Personas[personaId]) { - personaName = SC_Personas[personaId].Nombre || personaId; - } - document.getElementById(field_persona).value = personaName; - document.getElementById(field_notas).value = data['Notas'] || ''; - - // Load photo attachment if present - if (DB.getAttachment) { - DB.getAttachment('cajas_movimientos', movimientoId, 'foto') - .then((durl) => { - try { - if (durl) { - var fotoElement = document.getElementById(render_foto); - if (fotoElement) { - fotoElement.src = durl; - fotoElement.style.display = 'block'; - } - } - } catch (e) { - console.warn('Error setting foto:', e); - } - }) - .catch((e) => { - console.warn('Error loading foto:', e); - }); - } - } - - if (typeof data === 'string') { - TS_decrypt( - data, - SECRET, - (data, wasEncrypted) => { - load_data(data); - }, - 'cajas_movimientos', - movimientoId - ); - } else { - load_data(data || {}); - } - }); - - // Enable edit mode - function enableEditMode() { - isEditMode = true; - document.getElementById(field_fecha).disabled = false; - document.getElementById(field_tipo).disabled = false; - document.getElementById(field_monto).disabled = false; - document.getElementById(field_persona).disabled = false; - document.getElementById(field_notas).disabled = false; - document.getElementById(render_foto).style.cursor = 'pointer'; - - document.getElementById(btn_editar).style.display = 'none'; - document.getElementById(btn_volver).style.display = 'none'; - document.getElementById(btn_guardar).style.display = 'inline-block'; - document.getElementById(btn_cancelar).style.display = 'inline-block'; - document.getElementById(btn_borrar).style.display = 'none'; - } - - // Disable edit mode - function disableEditMode() { - isEditMode = false; - document.getElementById(field_fecha).disabled = true; - document.getElementById(field_tipo).disabled = true; - document.getElementById(field_monto).disabled = true; - document.getElementById(field_persona).disabled = true; - document.getElementById(field_notas).disabled = true; - document.getElementById(render_foto).style.cursor = 'default'; - - document.getElementById(btn_editar).style.display = checkRole('cajas:edit') ? 'inline-block' : 'none'; - document.getElementById(btn_volver).style.display = 'inline-block'; - document.getElementById(btn_guardar).style.display = 'none'; - document.getElementById(btn_cancelar).style.display = 'none'; - document.getElementById(btn_borrar).style.display = checkRole('cajas:edit') ? 'inline-block' : 'none'; - } - - // Button handlers - document.getElementById(btn_volver).onclick = () => { - setUrlHash('cajas,' + cajaId); - }; - - document.getElementById(btn_editar).onclick = () => { - enableEditMode(); - }; - - document.getElementById(btn_cancelar).onclick = () => { - disableEditMode(); - // Reload data to discard changes - DB.get('cajas_movimientos', movimientoId).then((data) => { - if (typeof data === 'string') { - TS_decrypt(data, SECRET, (d) => { load_data(d); }, 'cajas_movimientos', movimientoId); - } else { - load_data(data || {}); - } - }); - }; - - // Photo click handler - document.getElementById(render_foto).onclick = () => { - if (isEditMode) { - // In edit mode: upload new photo - document.getElementById(field_foto).click(); - } else { - // In read mode: open photo in new tab - var fotoElement = document.getElementById(render_foto); - if (fotoElement && fotoElement.src) { - window.open(fotoElement.src, '_blank'); - } - } - }; - - document.getElementById(field_foto).addEventListener('change', function (e) { - const file = e.target.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = function (ev) { - const url = ev.target.result; - document.getElementById(render_foto).src = url; - resized = url; - }; - reader.readAsDataURL(file); - }); - - // Save handler - document.getElementById(btn_guardar).onclick = () => { - var guardarBtn = document.getElementById(btn_guardar); - if (guardarBtn.disabled) return; - - var tipo = document.getElementById(field_tipo).value; - var monto = parseFloat(document.getElementById(field_monto).value); - var fecha = document.getElementById(field_fecha).value; - var notas = document.getElementById(field_notas).value; - - // Validation - if (!tipo) { - alert('Por favor selecciona el tipo de movimiento'); - return; - } - if (!monto || monto <= 0) { - alert('Por favor ingresa un monto válido'); - return; - } - if (!fecha) { - alert('Por favor selecciona una fecha'); - return; - } - - guardarBtn.disabled = true; - guardarBtn.style.opacity = '0.5'; - - var fechaISO = new Date(fecha).toISOString(); - - var data = { - Caja: movimientoData.Caja, - Fecha: fechaISO, - Tipo: tipo, - Monto: monto, - Persona: movimientoData.Persona, - Notas: notas, - }; - - // Preserve transfer destination if applicable - if (movimientoData.CajaDestino) { - data.CajaDestino = movimientoData.CajaDestino; - } - - document.getElementById('actionStatus').style.display = 'block'; - DB.put('cajas_movimientos', movimientoId, data) - .then(() => { - // Save photo attachment if a new one was provided - var attachPromise = Promise.resolve(true); - if (resized && resized.indexOf('data:') === 0) { - attachPromise = DB.putAttachment( - 'cajas_movimientos', - movimientoId, - 'foto', - resized, - 'image/png' - ); - } - - attachPromise - .then(() => { - toastr.success('Movimiento actualizado!'); - disableEditMode(); - document.getElementById('actionStatus').style.display = 'none'; - // Reload to show updates - setTimeout(() => { - PAGES.cajas.movimiento(cajaId, movimientoId); - }, SAVE_WAIT); - }) - .catch((e) => { - console.warn('Error saving:', e); - document.getElementById('actionStatus').style.display = 'none'; - guardarBtn.disabled = false; - guardarBtn.style.opacity = '1'; - toastr.error('Error al guardar el movimiento'); - }); - }) - .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 movimiento'); - }); - }; - - // Show/hide edit button based on permissions - if (checkRole('cajas:edit')) { - document.getElementById(btn_editar).style.display = 'inline-block'; - document.getElementById(btn_borrar).style.display = 'inline-block'; - } - - // Delete handler - document.getElementById(btn_borrar).onclick = () => { - if (confirm('¿Quieres borrar este movimiento?')) { - DB.del('cajas_movimientos', movimientoId).then(() => { - toastr.success('Movimiento borrado!'); - setTimeout(() => { - setUrlHash('cajas,' + cajaId); - }, SAVE_WAIT); - }); - } - }; - }, - - // Create new transaction (movimiento) - nuevo_movimiento: function (cajaId) { - if (!checkRole('cajas:edit')) { - setUrlHash('cajas,' + cajaId); - return; - } - - var field_fecha = safeuuid(); - var field_tipo = safeuuid(); - var field_monto = safeuuid(); - var field_persona = safeuuid(); - var field_notas = safeuuid(); - var field_foto = safeuuid(); - var render_foto = safeuuid(); - var field_caja_destino = safeuuid(); - var div_caja_destino = safeuuid(); - var btn_guardar = safeuuid(); - var btn_cancelar = safeuuid(); - - var resized = ''; - - container.innerHTML = html` -

Nuevo Movimiento

-
- - - - - - - -
- - -
- `; - - // Set current datetime - var now = new Date(); - var tzOffset = now.getTimezoneOffset() * 60000; - var localISOTime = new Date(now - tzOffset).toISOString().slice(0, 16); - document.getElementById(field_fecha).value = localISOTime; - - // Load personas for selection - var selectedPersona = ''; - var container_personas = document.querySelector('#personaSelector'); - addCategory_Personas( - container_personas, - SC_Personas, - selectedPersona, - (personaId) => { - document.getElementById(field_persona).value = personaId; - selectedPersona = personaId; - }, - 'Persona', - false, - '- No hay personas registradas -' - ); - - // Load cajas for destination selection - DB.map('cajas', (data, key) => { - function addCajaOption(cajaData, cajaKey) { - if (cajaKey === cajaId) return; // Don't show current caja - var select = document.getElementById(field_caja_destino); - if (!select) return; - var option = document.createElement('option'); - option.value = cajaKey; - option.textContent = cajaData.Nombre || cajaKey; - select.appendChild(option); - } - - if (typeof data === 'string') { - TS_decrypt( - data, - SECRET, - (cajaData, wasEncrypted) => { - addCajaOption(cajaData, key); - }, - 'cajas', - key - ); - } else { - addCajaOption(data, key); - } - }); - - // Show/hide destination caja based on transaction type - document.getElementById(field_tipo).addEventListener('change', function () { - var tipo = this.value; - var divDestino = document.getElementById(div_caja_destino); - if (tipo === 'Transferencia') { - divDestino.style.display = 'block'; - } else { - divDestino.style.display = 'none'; - } - }); - - // Photo upload handler (click image to upload) - document.getElementById(render_foto).onclick = () => { - document.getElementById(field_foto).click(); - }; - - document.getElementById(field_foto).addEventListener('change', function (e) { - const file = e.target.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = function (ev) { - const url = ev.target.result; - document.getElementById(render_foto).src = url; - resized = url; - }; - reader.readAsDataURL(file); - }); - - document.getElementById(btn_guardar).onclick = () => { - var guardarBtn = document.getElementById(btn_guardar); - if (guardarBtn.disabled) return; - - var tipo = document.getElementById(field_tipo).value; - var monto = parseFloat(document.getElementById(field_monto).value); - var personaId = document.getElementById(field_persona).value; - var fecha = document.getElementById(field_fecha).value; - var notas = document.getElementById(field_notas).value; - var cajaDestinoId = document.getElementById(field_caja_destino).value; - - // Validation - if (!tipo) { - alert('Por favor selecciona el tipo de movimiento'); - return; - } - if (!monto || monto <= 0) { - alert('Por favor ingresa un monto válido'); - return; - } - if (!personaId) { - alert('Por favor selecciona una persona'); - return; - } - if (!fecha) { - alert('Por favor selecciona una fecha'); - return; - } - - // Validate destination caja for transfers - if (tipo === 'Transferencia') { - if (!cajaDestinoId) { - alert('Por favor selecciona la caja destino para la transferencia'); - return; - } - if (cajaDestinoId === cajaId) { - alert('No puedes transferir a la misma caja'); - return; - } - } - - // Validate photo for expenses - if (tipo === 'Gasto' && !resized) { - alert('La foto del ticket es obligatoria para gastos'); - return; - } - - guardarBtn.disabled = true; - guardarBtn.style.opacity = '0.5'; - - var movimientoId = safeuuid(''); - var fechaISO = new Date(fecha).toISOString(); - - var data = { - Caja: cajaId, - Fecha: fechaISO, - Tipo: tipo, - Monto: monto, - Persona: personaId, - Notas: notas, - }; - - // Add destination caja for transfers - if (tipo === 'Transferencia') { - data.CajaDestino = cajaDestinoId; - } - - document.getElementById('actionStatus').style.display = 'block'; - DB.put('cajas_movimientos', movimientoId, data) - .then(() => { - // Save photo attachment if present - var attachPromise = Promise.resolve(true); - if (resized && resized.indexOf('data:') === 0) { - attachPromise = DB.putAttachment( - 'cajas_movimientos', - movimientoId, - 'foto', - resized, - 'image/png' - ); - } - - attachPromise - .then(() => { - // Update source caja balance - return updateCajaBalance(cajaId, tipo, monto); - }) - .then(() => { - // If transfer, update destination caja balance - if (tipo === 'Transferencia' && cajaDestinoId) { - return updateCajaBalance(cajaDestinoId, 'Ingreso', monto); - } - return Promise.resolve(); - }) - .then(() => { - toastr.success('Movimiento guardado!'); - setTimeout(() => { - document.getElementById('actionStatus').style.display = 'none'; - setUrlHash('cajas,' + cajaId); - }, SAVE_WAIT); - }) - .catch((e) => { - console.warn('Error saving:', e); - document.getElementById('actionStatus').style.display = 'none'; - guardarBtn.disabled = false; - guardarBtn.style.opacity = '1'; - toastr.error('Error al guardar el movimiento'); - }); - }) - .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 movimiento'); - }); - }; - - document.getElementById(btn_cancelar).onclick = () => { - setUrlHash('cajas,' + cajaId); - }; - - function updateCajaBalance(cajaId, tipo, monto) { - return DB.get('cajas', cajaId).then((caja) => { - function updateBalance(cajaData) { - var currentBalance = parseFloat(cajaData.Balance || 0); - var newBalance = currentBalance; - - if (tipo === 'Ingreso') { - newBalance = currentBalance + monto; - } else if (tipo === 'Gasto' || tipo === 'Transferencia') { - // For transfers, this updates the source caja (deduct amount) - newBalance = currentBalance - monto; - } - - cajaData.Balance = fixfloat(newBalance); - return DB.put('cajas', cajaId, cajaData); - } - - if (typeof caja === 'string') { - return new Promise((resolve, reject) => { - TS_decrypt( - caja, - SECRET, - (cajaData, wasEncrypted) => { - updateBalance(cajaData).then(resolve).catch(reject); - }, - 'cajas', - cajaId - ); - }); - } else { - return updateBalance(caja || {}); - } - }); - } - }, - - // View/edit a cash register (caja) - edit: function (mid) { - if (!checkRole('cajas')) { - setUrlHash('cajas'); - return; - } - - // Check for special routes - var parts = location.hash.split("?")[0].split(','); - if (parts[2] === 'movimientos' && parts[3] === '_nuevo') { - PAGES.cajas.nuevo_movimiento(parts[1]); - return; - } - if (parts[2] === 'movimiento' && parts[3]) { - PAGES.cajas.movimiento(parts[1], parts[3]); - return; - } - - var nameh1 = safeuuid(); - var field_nombre = safeuuid(); - var field_balance = safeuuid(); - var field_notas = safeuuid(); - var btn_guardar = safeuuid(); - var btn_borrar = safeuuid(); - var btn_nuevo_movimiento = safeuuid(); - var movimientos_container = safeuuid(); - - var isMonederos = mid === 'monederos'; - - container.innerHTML = html` -

${isMonederos ? 'Monederos' : 'Caja'}

- ${isMonederos ? '' : BuildQR('cajas,' + mid, 'Esta Caja')} -
- - - -
- ${ - isMonederos - ? '' - : html` - - - ` - } -
- -

Movimientos de ${isMonederos ? 'Monederos' : 'esta Caja'}

- ${ - isMonederos - ? html`

Aquí se muestran todas las transacciones de los monederos (módulo Pagos)

` - : html`` - } -
- `; - - // Load caja data - if (!isMonederos) { - DB.get('cajas', mid).then((data) => { - function load_data(data) { - document.getElementById(nameh1).innerText = mid; - document.getElementById(field_nombre).value = data['Nombre'] || ''; - document.getElementById(field_balance).value = data['Balance'] || 0; - document.getElementById(field_notas).value = data['Notas'] || ''; - } - - if (typeof data === 'string') { - TS_decrypt( - data, - SECRET, - (data, wasEncrypted) => { - load_data(data); - }, - 'cajas', - mid - ); - } else { - load_data(data || {}); - } - }); - - document.getElementById(btn_guardar).onclick = () => { - var guardarBtn = document.getElementById(btn_guardar); - if (guardarBtn.disabled) return; - - guardarBtn.disabled = true; - guardarBtn.style.opacity = '0.5'; - - var data = { - Nombre: document.getElementById(field_nombre).value, - Balance: parseFloat(document.getElementById(field_balance).value) || 0, - Notas: document.getElementById(field_notas).value, - }; - - document.getElementById('actionStatus').style.display = 'block'; - DB.put('cajas', mid, data) - .then(() => { - toastr.success('Guardado!'); - setTimeout(() => { - document.getElementById('actionStatus').style.display = 'none'; - setUrlHash('cajas'); - }, 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 caja'); - }); - }; - - document.getElementById(btn_borrar).onclick = () => { - if (confirm('¿Quieres borrar esta caja? Los movimientos no se borrarán.')) { - DB.del('cajas', mid).then(() => { - toastr.error('Caja borrada!'); - setTimeout(() => { - setUrlHash('cajas'); - }, SAVE_WAIT); - }); - } - }; - - document.getElementById(btn_nuevo_movimiento).onclick = () => { - setUrlHash('cajas,' + mid + ',_nuevo'); - }; - } else { - // Monederos - show aggregated wallet data - document.getElementById(nameh1).innerText = 'Monederos'; - document.getElementById(field_nombre).value = 'Monederos (Tarjetas)'; - - // Calculate total balance from all personas - var totalBalance = 0; - Object.values(SC_Personas).forEach((persona) => { - totalBalance += parseFloat(persona.Monedero_Balance || 0); - }); - document.getElementById(field_balance).value = fixfloat(totalBalance); - document.getElementById(field_notas).value = 'Movimientos de todos los monederos del sistema'; - } - - // Load movements for this caja (or all pagos for monederos) - if (isMonederos) { - // Show pagos transactions - const config = [ - { - key: 'Fecha', - label: 'Fecha', - type: 'template', - template: (data, element) => { - var fecha = data.Fecha || ''; - if (fecha) { - var d = new Date(fecha); - element.innerText = d.toLocaleDateString() + ' ' + d.toLocaleTimeString(); - } - }, - default: '', - }, - { key: 'Tipo', label: 'Tipo', type: 'text', default: '' }, - { - key: 'Monto', - label: 'Monto', - type: 'template', - template: (data, element) => { - var tipo = data.Tipo || ''; - var monto = parseFloat(data.Monto || 0); - var sign = tipo === 'Ingreso' ? '+' : '-'; - var color = tipo === 'Ingreso' ? 'green' : 'red'; - element.innerHTML = html`${sign}${monto.toFixed(2)}€`; - }, - default: '0.00€', - }, - { - key: 'Persona', - label: 'Monedero', - type: 'persona-nombre', - default: '', - }, - { key: 'Metodo', label: 'Método', type: 'text', default: '' }, - { key: 'Notas', label: 'Notas', type: 'text', default: '' }, - ]; - - TS_IndexElement( - 'pagos', - config, - 'pagos', - document.getElementById(movimientos_container), - function (data, new_tr) { - new_tr.onclick = () => { - setUrlHash('pagos,' + data._key); - }; - }, - undefined, - true - ); - } else { - // Show cajas_movimientos for this specific caja - const config = [ - { - key: 'Fecha', - label: 'Fecha', - type: 'template', - template: (data, element) => { - var fecha = data.Fecha || ''; - if (fecha) { - var d = new Date(fecha); - element.innerText = d.toLocaleDateString() + ' ' + d.toLocaleTimeString(); - } - }, - default: '', - }, - { key: 'Tipo', label: 'Tipo', type: 'text', default: '' }, - { - key: 'Monto', - label: 'Monto', - type: 'template', - template: (data, element) => { - var tipo = data.Tipo || ''; - var monto = parseFloat(data.Monto || 0); - var sign = tipo === 'Ingreso' ? '+' : tipo === 'Gasto' ? '-' : '<->'; - var color = tipo === 'Ingreso' ? 'green' : tipo === 'Gasto' ? 'red' : 'blue'; - element.innerHTML = html`${sign}${monto.toFixed(2)}€`; - }, - default: '0.00€', - }, - { - key: 'Persona', - label: 'Persona', - type: 'persona-nombre', - default: '', - }, - { key: 'Notas', label: 'Notas', type: 'text', default: '' }, - ]; - - TS_IndexElement( - 'cajas,' + mid + ',movimiento', - config, - 'cajas_movimientos', - document.getElementById(movimientos_container), - function (data, new_tr) { - new_tr.onclick = () => { - setUrlHash('cajas,' + mid + ',movimiento,' + data._key); - }; - }, - function (data) { - // Filter: only show movements for this caja (return true to HIDE the row) - return data.Caja !== mid; - }, - true - ); - } - }, - - // List all cash registers - index: function () { - if (!checkRole('cajas')) { - setUrlHash('index'); - return; - } - - var btn_new = safeuuid(); - var btn_monederos = safeuuid(); - var tableContainer = safeuuid(); - - container.innerHTML = html` -

Cajas

- - -
- `; - - const config = [ - { key: 'Nombre', label: 'Nombre', type: 'text', default: '' }, - { - key: 'Balance', - label: 'Balance', - type: 'template', - template: (data, element) => { - var balance = parseFloat(data.Balance || 0); - var color = balance >= 0 ? 'green' : 'red'; - element.innerHTML = html`${balance.toFixed(2)}€`; - }, - default: '0.00€', - }, - { key: 'Notas', label: 'Notas', type: 'text', default: '' }, - ]; - - TS_IndexElement( - 'cajas', - config, - 'cajas', - document.getElementById(tableContainer), - undefined, - undefined, - true - ); - - document.getElementById(btn_monederos).onclick = () => { - setUrlHash('cajas,monederos'); - }; - - if (!checkRole('cajas:edit')) { - document.getElementById(btn_new).style.display = 'none'; - } else { - document.getElementById(btn_new).onclick = () => { - setUrlHash('cajas,' + safeuuid('')); - }; - } - }, -}; diff --git a/src/page/index.js b/src/page/index.js index 3cd5052..e297024 100644 --- a/src/page/index.js +++ b/src/page/index.js @@ -11,6 +11,8 @@ PAGES.index = { Utiliza el menú superior para abrir un modulo

+ +

`; }, diff --git a/src/page/panel.js b/src/page/panel.js new file mode 100644 index 0000000..ff643d2 --- /dev/null +++ b/src/page/panel.js @@ -0,0 +1,357 @@ +PERMS['panel'] = 'Panel'; +PAGES.panel = { + navcss: 'btn2', + icon: 'static/appico/calendar.png', + AccessControl: true, + Title: 'Panel', + + index: function () { + if (!checkRole('panel')) { + setUrlHash('index'); + return; + } + + var contentId = safeuuid(); + container.innerHTML = html` +

Panel de acogida del día

+

Quiz de aprendizaje con retroalimentación para empezar la jornada.

+
Cargando datos del día...
+ `; + + PAGES.panel + .__buildDailyContext() + .then((ctx) => { + var questions = PAGES.panel.__buildQuestions(ctx); + PAGES.panel.__renderQuiz(contentId, ctx, questions); + }) + .catch((e) => { + console.warn('Panel load error', e); + document.getElementById(contentId).innerHTML = + 'No se pudo cargar el Panel ahora mismo.'; + }); + }, + + __decryptIfNeeded: function (table, id, raw) { + return new Promise((resolve) => { + if (typeof raw !== 'string') { + resolve(raw || {}); + return; + } + TS_decrypt( + raw, + SECRET, + (data) => { + resolve(data || {}); + }, + table, + id + ); + }); + }, + + __getTodayComedor: async function () { + var rows = await DB.list('comedor'); + var today = CurrentISODate(); + var items = []; + + for (var i = 0; i < rows.length; i++) { + var row = rows[i]; + var data = await PAGES.panel.__decryptIfNeeded('comedor', row.id, row.data); + if ((data.Fecha || '') === today) { + items.push(data); + } + } + + if (items.length === 0) { + return { + Primero: '', + Segundo: '', + Postre: '', + Tipo: '', + }; + } + + items.sort((a, b) => { + var ta = (a.Tipo || '').toLowerCase(); + var tb = (b.Tipo || '').toLowerCase(); + return ta < tb ? -1 : 1; + }); + + return items[0] || {}; + }, + + __getNotaById: async function (id) { + var data = await DB.get('notas', id); + if (!data) return {}; + return await PAGES.panel.__decryptIfNeeded('notas', id, data); + }, + + __getDiarioHoy: async function () { + var did = 'diario-' + CurrentISODate(); + var data = await DB.get('aulas_informes', did); + if (!data) return {}; + return await PAGES.panel.__decryptIfNeeded('aulas_informes', did, data); + }, + + __extractFirstLine: function (text) { + var lines = String(text || '') + .split('\n') + .map((x) => x.trim()) + .filter((x) => x !== ''); + return lines[0] || ''; + }, + + __buildDailyContext: async function () { + var comedor = await PAGES.panel.__getTodayComedor(); + var tareas = await PAGES.panel.__getNotaById('tareas'); + var diario = await PAGES.panel.__getDiarioHoy(); + + var planHoy = + PAGES.panel.__extractFirstLine(tareas.Contenido) || + PAGES.panel.__extractFirstLine(diario.Contenido) || + 'Revisar rutinas, colaborar y participar en las actividades del aula.'; + + return { + fecha: CurrentISODate(), + comedor: { + primero: (comedor.Primero || '').trim(), + segundo: (comedor.Segundo || '').trim(), + postre: (comedor.Postre || '').trim(), + tipo: (comedor.Tipo || '').trim(), + }, + planHoy: planHoy, + }; + }, + + __pickDistractors: function (correct, pool, count) { + var options = []; + var seen = {}; + var cleanCorrect = (correct || '').trim(); + + pool.forEach((item) => { + var text = String(item || '').trim(); + if (text === '' || text === cleanCorrect || seen[text]) return; + seen[text] = true; + options.push(text); + }); + + var out = []; + for (var i = 0; i < options.length && out.length < count; i++) { + out.push(options[i]); + } + + while (out.length < count) { + out.push('No aplica hoy'); + } + + return out; + }, + + __shuffle: function (arr) { + var copy = arr.slice(); + for (var i = copy.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + var tmp = copy[i]; + copy[i] = copy[j]; + copy[j] = tmp; + } + return copy; + }, + + __buildQuestions: function (ctx) { + var c = ctx.comedor || {}; + var poolComedor = [c.primero, c.segundo, c.postre, 'No hay menú registrado']; + var questions = []; + + if (c.primero) { + var opts1 = [c.primero].concat(PAGES.panel.__pickDistractors(c.primero, poolComedor, 3)); + questions.push({ + id: 'q-comida-primero', + text: '¿Qué hay de comer hoy de primero?', + options: PAGES.panel.__shuffle(opts1), + correct: c.primero, + ok: '¡Correcto! Ya sabes el primer plato de hoy.', + bad: 'Repasa el menú del día para anticipar la comida.', + }); + } + + if (c.segundo) { + var opts2 = [c.segundo].concat(PAGES.panel.__pickDistractors(c.segundo, poolComedor, 3)); + questions.push({ + id: 'q-comida-segundo', + text: '¿Y de segundo, qué toca?', + options: PAGES.panel.__shuffle(opts2), + correct: c.segundo, + ok: '¡Bien! Segundo identificado.', + bad: 'Casi. Mira el módulo Comedor para recordar el segundo plato.', + }); + } + + if (c.postre) { + var opts3 = [c.postre].concat(PAGES.panel.__pickDistractors(c.postre, poolComedor, 3)); + questions.push({ + id: 'q-comida-postre', + text: '¿Cuál es el postre de hoy?', + options: PAGES.panel.__shuffle(opts3), + correct: c.postre, + ok: '¡Perfecto! Postre acertado.', + bad: 'No pasa nada, revisa el postre en el menú diario.', + }); + } + + var plan = ctx.planHoy || ''; + var distractPlan = [ + 'No hay actividades planificadas hoy', + 'Solo descanso todo el día', + 'Actividad libre sin objetivos', + ]; + var planOptions = [plan].concat(PAGES.panel.__pickDistractors(plan, distractPlan, 3)); + questions.push({ + id: 'q-plan-hoy', + text: '¿Qué vamos a hacer hoy?', + options: PAGES.panel.__shuffle(planOptions), + correct: plan, + ok: '¡Muy bien! Tienes claro el plan del día.', + bad: 'Revisa las tareas/diario para conocer el plan del día.', + }); + + if (questions.length === 0) { + questions.push({ + id: 'q-fallback', + text: 'No hay menú cargado. ¿Qué acción es correcta ahora?', + options: [ + 'Consultar el módulo Comedor y las Notas del día', + 'Ignorar la planificación diaria', + 'Esperar sin revisar información', + 'Saltar la acogida', + ], + correct: 'Consultar el módulo Comedor y las Notas del día', + ok: 'Correcto. Ese es el siguiente paso recomendado.', + bad: 'La acogida mejora si revisamos menú y planificación diaria.', + }); + } + + return questions; + }, + + __renderQuiz: function (contentId, ctx, questions) { + var target = document.getElementById(contentId); + var state = { + idx: 0, + answers: {}, + score: 0, + feedback: '', + }; + + function saveResult() { + var rid = CurrentISOTime() + '-' + safeuuid(''); + var payload = { + Fecha: ctx.fecha, + Persona: SUB_LOGGED_IN_ID || '', + Aciertos: state.score, + Total: questions.length, + Respuestas: state.answers, + }; + DB.put('panel_respuestas', rid, payload); + } + + function renderCurrent() { + var q = questions[state.idx]; + if (!q) return; + + var selected = state.answers[q.id] || ''; + var optionsHtml = q.options + .map((option, i) => { + var oid = safeuuid(); + var checked = selected === option ? 'checked' : ''; + return ` + + `; + }) + .join(''); + + target.innerHTML = html` +
+ Pregunta ${state.idx + 1} de ${questions.length} +
${q.text}
+ + Menú hoy: ${ctx.comedor.primero || '—'} / ${ctx.comedor.segundo || '—'} / + ${ctx.comedor.postre || '—'} + +
${optionsHtml}
+
${state.feedback || ''}
+
+ + +
+
+ `; + + document.getElementById('panel-cancel').onclick = () => setUrlHash('index'); + document.getElementById('panel-next').onclick = () => { + var checked = document.querySelector('input[name="panel-question"]:checked'); + if (!checked) { + state.feedback = 'Selecciona una opción antes de continuar.'; + renderCurrent(); + return; + } + + var answer = checked.value; + state.answers[q.id] = answer; + + var wasCorrect = answer === q.correct; + if (wasCorrect) { + state.score++; + state.feedback = '✅ ' + q.ok; + } else { + state.feedback = '❌ ' + q.bad + ' Respuesta esperada: ' + q.correct; + } + + if (state.idx < questions.length - 1) { + state.idx++; + setTimeout(() => { + state.feedback = ''; + renderCurrent(); + }, 350); + return; + } + + saveResult(); + renderFinal(); + }; + } + + function renderFinal() { + var total = questions.length; + var ratio = total > 0 ? Math.round((state.score / total) * 100) : 0; + var msg = 'Buen trabajo. Sigue reforzando la acogida diaria.'; + if (ratio >= 80) msg = 'Excelente acogida: gran comprensión del día.'; + else if (ratio >= 50) msg = 'Buen avance. Revisa comedor/tareas para reforzar.'; + + target.innerHTML = html` +
+ Resultado del Panel +

${state.score} / ${total} aciertos (${ratio}%)

+

${msg}

+

Plan de hoy: ${ctx.planHoy}

+ + +
+ `; + + document.getElementById('panel-repeat').onclick = () => { + state.idx = 0; + state.answers = {}; + state.score = 0; + state.feedback = ''; + renderCurrent(); + }; + document.getElementById('panel-home').onclick = () => setUrlHash('index'); + } + + renderCurrent(); + }, +}; diff --git a/src/pwa.js b/src/pwa.js index 639464d..5929fd3 100644 --- a/src/pwa.js +++ b/src/pwa.js @@ -1,5 +1,130 @@ let newWorker; +const APP_VERSION = '%%VERSIONCO%%'; +const VERSION_CHECK_INTERVAL_MS = 10 * 60 * 1000; +let lastVersionCheckTs = 0; +let updatePromptShown = false; +function sendCouchUrlPrefixToServiceWorker(registration) { + if (!registration) { + return; + } + + const couchUrlPrefix = (localStorage.getItem('TELESEC_COUCH_URL') || '').trim(); + const message = { + type: 'SET_COUCH_URL_PREFIX', + url: couchUrlPrefix + }; + + if (registration.active) { + registration.active.postMessage(message); + } + if (registration.waiting) { + registration.waiting.postMessage(message); + } + if (registration.installing) { + registration.installing.postMessage(message); + } +} + +async function checkAppVersion(force = false) { + const now = Date.now(); + if (!force && now - lastVersionCheckTs < VERSION_CHECK_INTERVAL_MS) { + return; + } + if (!navigator.onLine) { + return; + } + + lastVersionCheckTs = now; + + try { + const response = await fetch(`/version.json?t=${Date.now()}`, { + cache: 'no-cache' + }); + + if (!response.ok) { + return; + } + + const data = await response.json(); + if (!data || !data.version) { + return; + } + + if (data.version !== APP_VERSION) { + const registration = await navigator.serviceWorker.getRegistration(); + if (registration) { + await registration.update(); + } + if (!updatePromptShown) { + showUpdateBar(); + updatePromptShown = true; + } + } else { + updatePromptShown = false; + } + } catch (error) { + console.warn('No se pudo comprobar la versión remota:', error); + } +} + +async function ActualizarProgramaTeleSec() { + if (!confirm('Se borrará la caché local del programa y se recargará la aplicación. ¿Continuar?')) { + return; + } + + let cacheCleared = true; + + try { + if ('serviceWorker' in navigator) { + const registration = await navigator.serviceWorker.getRegistration(); + if (registration) { + await registration.update(); + const sendSkipWaiting = (worker) => { + worker.postMessage({ type: 'SKIP_WAITING' }); + }; + + if (registration.waiting) { + sendSkipWaiting(registration.waiting); + } else if (registration.installing) { + await new Promise((resolve) => { + const installingWorker = registration.installing; + const onStateChange = () => { + if (installingWorker.state === 'installed') { + installingWorker.removeEventListener('statechange', onStateChange); + sendSkipWaiting(installingWorker); + resolve(); + } + }; + + installingWorker.addEventListener('statechange', onStateChange); + onStateChange(); + setTimeout(resolve, 2500); + }); + } + } + } + + if ('caches' in window) { + const cacheNames = await caches.keys(); + await Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName))); + } + } catch (error) { + cacheCleared = false; + console.error('No se pudo limpiar la caché completamente:', error); + if (typeof toastr !== 'undefined') { + toastr.error('No se pudo limpiar toda la caché. Recargando igualmente...'); + } + } + + if (cacheCleared && typeof toastr !== 'undefined') { + toastr.success('Caché limpiada. Recargando aplicación...'); + } + + setTimeout(() => { + location.reload(); + }, 700); +} function showUpdateBar() { let snackbar = document.getElementById('snackbar'); snackbar.className = 'show'; @@ -8,30 +133,41 @@ function showUpdateBar() { // The click event on the pop up notification document.getElementById('reload').addEventListener('click', function () { setTimeout(() => { - removeCache(); + ActualizarProgramaTeleSec(); }, 1000); - newWorker.postMessage({ action: 'skipWaiting' }); + if (newWorker) { + newWorker.postMessage({ type: 'SKIP_WAITING' }); + } }); if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('sw.js').then((reg) => { + const wireRegistration = (reg) => { reg.addEventListener('updatefound', () => { - // A wild service worker has appeared in reg.installing! newWorker = reg.installing; newWorker.addEventListener('statechange', () => { - // Has network.state changed? switch (newWorker.state) { case 'installed': if (navigator.serviceWorker.controller) { - // new update available showUpdateBar(); } - // No update available break; } }); }); + }; + + navigator.serviceWorker.getRegistration().then(async (reg) => { + if (!reg) { + reg = await navigator.serviceWorker.register('sw.js'); + } else { + await reg.update(); + } + + wireRegistration(reg); + sendCouchUrlPrefixToServiceWorker(reg); + checkAppVersion(true); + setInterval(checkAppVersion, VERSION_CHECK_INTERVAL_MS); }); let refreshing; @@ -40,4 +176,20 @@ if ('serviceWorker' in navigator) { window.location.reload(); refreshing = true; }); + + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + checkAppVersion(); + } + }); + + window.addEventListener('storage', (event) => { + if (event.key !== 'TELESEC_COUCH_URL') { + return; + } + + navigator.serviceWorker.getRegistration().then((registration) => { + sendCouchUrlPrefixToServiceWorker(registration); + }); + }); } diff --git a/src/sw.js b/src/sw.js index 3bdc5b2..7638462 100644 --- a/src/sw.js +++ b/src/sw.js @@ -1,10 +1,29 @@ var CACHE = 'telesec_%%VERSIONCO%%'; importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js'); +let couchUrlPrefix = ''; + +function normalizePrefix(url) { + if (!url || typeof url !== 'string') { + return ''; + } + + const trimmedUrl = url.trim(); + if (!trimmedUrl) { + return ''; + } + + return trimmedUrl.replace(/\/+$/, ''); +} + self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } + + if (event.data && event.data.type === 'SET_COUCH_URL_PREFIX') { + couchUrlPrefix = normalizePrefix(event.data.url); + } }); // workbox.routing.registerRoute( @@ -16,8 +35,17 @@ self.addEventListener('message', (event) => { // All but couchdb workbox.routing.registerRoute( - ({ url }) => !url.pathname.startsWith('/_couchdb/') && url.origin === self.location.origin, - new workbox.strategies.NetworkFirst({ + ({ request, url }) => { + const requestUrl = request && request.url ? request.url : url.href; + const normalizedRequestUrl = normalizePrefix(requestUrl); + + if (couchUrlPrefix && normalizedRequestUrl.startsWith(couchUrlPrefix)) { + return false; + } + + return !url.pathname.startsWith('/_couchdb/') && url.origin === self.location.origin; + }, + new workbox.strategies.CacheFirst({ cacheName: CACHE, }) ); diff --git a/src/version.json b/src/version.json new file mode 100644 index 0000000..6287af9 --- /dev/null +++ b/src/version.json @@ -0,0 +1,3 @@ +{ + "version": "%%VERSIONCO%%" +}