Refactor code structure for improved readability and maintainability

This commit is contained in:
Naiel
2026-03-01 23:37:37 +00:00
parent 9a760a1d24
commit 8b7d0258ae
15 changed files with 470 additions and 74 deletions

20
assets/static/chart.umd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -451,4 +451,24 @@ pre {
.panel-option:has(input:checked) {
background-color: #ccc;
outline: 5px solid blue;
}
.saveico {
border-color: green !important;
}
.delico {
border-color: red !important;
}
.opicon {
border-color: blue !important;
}
.saveico img, .delico img, .opicon img {
height: 52px;
vertical-align: middle;
}
.saveico, .delico, .opicon {
padding: 2.5px 7.5px;
background: transparent;
border-radius: 10px;
border: 4px solid;
}

BIN
assets/static/exchange.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
assets/static/exit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
assets/static/find.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
assets/static/garbage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
assets/static/printer2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -74,6 +74,7 @@
<script src="static/pouchdb.min.js"></script>
<script src="static/toastr.min.js"></script>
<script src="static/doublescroll.js"></script>
<script src="static/chart.umd.min.js"></script>
<script src="pwa.js"></script>
<script src="config.js"></script>
<script src="db.js"></script>

View File

@@ -109,8 +109,14 @@ PAGES.comedor = {
<input type="text" id="${field_postre}" value="" /><br />
<div class="picto" id="${btn_picto_postre}"></div>
</label>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
<button class="saveico" id="${btn_guardar}">
<img src="static/floppy_disk_green.png" />
<br>Guardar
</button>
<button class="delico" id="${btn_borrar}">
<img src="static/garbage.png" />
<br>Borrar
</button>
</fieldset>
`;
const pictogramSelector = TS_CreateArasaacSelector({

View File

@@ -11,10 +11,13 @@ PAGES.index = {
Tienes ${parseFloat(SUB_LOGGED_IN_DETAILS.Monedero_Balance).toPrecision(2)} € en el
monedero.
</h2>
<div
id="${div_stats}"
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 15px; margin-bottom: 20px;"
></div>
<details style="border: 2px solid black; padding: 15px; border-radius: 10px;">
<summary>Estadisticas</summary>
<div
id="${div_stats}"
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 15px; margin-bottom: 20px;"
></div>
</details>
<em>Utiliza el menú superior para abrir un modulo</em>
<br /><br />
<button class="btn1" onclick="ActualizarProgramaTeleSec()">Actualizar programa</button>

View File

@@ -18,67 +18,323 @@ PAGES.materiales = {
var field_cantidad_min = safeuuid();
var field_ubicacion = safeuuid();
var field_notas = safeuuid();
var mov_tipo = safeuuid();
var mov_cantidad = safeuuid();
var mov_nota = safeuuid();
var mov_btn = safeuuid();
var mov_registro = safeuuid();
var mov_chart = safeuuid();
var mov_chart_canvas = safeuuid();
var mov_open_modal_btn = safeuuid();
var btn_print_chart = safeuuid();
var mov_modal = safeuuid();
var mov_modal_close = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var FECHA_ISO = new Date().toISOString().split('T')[0];
var movimientos = [];
var movimientosChartInstance = null;
function parseNum(v, fallback = 0) {
var n = parseFloat(v);
return Number.isFinite(n) ? n : fallback;
}
function buildMaterialData() {
return {
Nombre: document.getElementById(field_nombre).value,
Unidad: document.getElementById(field_unidad).value,
Cantidad: document.getElementById(field_cantidad).value,
Cantidad_Minima: document.getElementById(field_cantidad_min).value,
Ubicacion: document.getElementById(field_ubicacion).value,
Revision: document.getElementById(field_revision).value,
Notas: document.getElementById(field_notas).value,
Movimientos: movimientos,
};
}
function renderMovimientos() {
var el = document.getElementById(mov_registro);
if (!el) return;
if (!movimientos.length) {
el.innerHTML = '<small>Sin movimientos registrados.</small>';
return;
}
var rows = movimientos
.map((mov) => {
var fecha = mov.Fecha ? new Date(mov.Fecha).toLocaleString('es-ES') : '-';
return html`<tr>
<td>${fecha}</td>
<td>${mov.Tipo || '-'}</td>
<td>${mov.Cantidad ?? '-'}</td>
<td>${mov.Antes ?? '-'}</td>
<td>${mov.Despues ?? '-'}</td>
<td>${mov.Nota || ''}</td>
</tr>`;
})
.join('');
el.innerHTML = html`
<table>
<thead>
<tr>
<th>Fecha</th>
<th>Tipo</th>
<th>Cantidad</th>
<th>Antes</th>
<th>Después</th>
<th>Nota</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
`;
}
function renderMovimientosChart() {
var el = document.getElementById(mov_chart);
if (!el) return;
if (movimientosChartInstance) {
movimientosChartInstance.destroy();
movimientosChartInstance = null;
}
if (!movimientos.length) {
el.innerHTML = html`
<h3 style="margin: 0 0 8px 0;">Historial de movimientos por fecha</h3>
<small>Sin datos para graficar.</small>
`;
return;
}
var ordered = [...movimientos].sort((a, b) => {
return new Date(a.Fecha || 0).getTime() - new Date(b.Fecha || 0).getTime();
});
var deltas = [];
var labelsShort = [];
ordered.forEach((mov) => {
var cantidad = parseNum(mov.Cantidad, 0);
var delta = 0;
if (mov.Tipo === 'Entrada') {
delta = cantidad;
} else if (mov.Tipo === 'Salida') {
delta = -cantidad;
} else {
var antes = parseNum(mov.Antes, 0);
var despues = parseNum(mov.Despues, antes);
delta = despues - antes;
}
deltas.push(Number(delta.toFixed(2)));
var fechaTxt = mov.Fecha ? new Date(mov.Fecha).toLocaleString('es-ES') : '-';
labelsShort.push(fechaTxt);
});
var currentStock = parseNum(document.getElementById(field_cantidad)?.value, 0);
var totalNeto = deltas.reduce((acc, n) => acc + n, 0);
var stockInicialInferido = currentStock - totalNeto;
if (ordered.length > 0 && Number.isFinite(parseNum(ordered[0].Antes, NaN))) {
stockInicialInferido = parseNum(ordered[0].Antes, stockInicialInferido);
}
var acumulado = stockInicialInferido;
var values = deltas.map((neto) => {
acumulado += neto;
return Number(acumulado.toFixed(2));
});
el.innerHTML = html`
<h3 style="margin: 0 0 8px 0;">Historial de movimientos por fecha</h3>
<small style="display: block;margin-bottom: 6px;">Stock por fecha (cierre diario)</small>
<canvas id="${mov_chart_canvas}" style="width: 100%;height: 280px;"></canvas>
`;
if (typeof Chart === 'undefined') {
el.innerHTML += '<small>No se pudo cargar la librería de gráficos.</small>';
return;
}
var chartCanvasEl = document.getElementById(mov_chart_canvas);
if (!chartCanvasEl) return;
movimientosChartInstance = new Chart(chartCanvasEl, {
type: 'line',
data: {
labels: labelsShort,
datasets: [
{
label: 'Stock diario',
data: values,
borderColor: '#2d7ef7',
backgroundColor: 'rgba(45,126,247,0.16)',
fill: true,
tension: 0.25,
pointRadius: 3,
pointHoverRadius: 4,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
display: false,
},
y: {
title: {
display: false,
text: 'Stock',
},
grace: '10%',
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
},
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false,
},
},
});
}
container.innerHTML = html`
<h1>Material <code id="${nameh1}"></code></h1>
${BuildQR('materiales,' + mid)}
<fieldset>
<label>
Fecha Revisión<br />
<input type="date" id="${field_revision}" />
<a
onclick='document.getElementById("${field_revision}").value = "${FECHA_ISO}";'
style="color: blue;cursor: pointer;font-size: 0.9em;"
>Hoy - Contado todas las existencias </a
><br /><br />
</label>
<label>
Nombre<br />
<input type="text" id="${field_nombre}" /><br /><br />
</label>
<label>
Unidad<br />
<select id="${field_unidad}">
<option value="unidad(es)">unidad(es)</option>
<option value="paquete(s)">paquete(s)</option>
<option value="caja(s)">caja(s)</option>
<option value="rollo(s)">rollo(s)</option>
<option value="bote(s)">bote(s)</option>
<fieldset style="width: 100%;max-width: 980px;box-sizing: border-box;">
<div style="display: flex;flex-wrap: wrap;gap: 10px 16px;align-items: flex-end;">
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
<label for="${field_revision}">Fecha Revisión</label>
<input type="date" id="${field_revision}" style="flex: 1;" />
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
<label for="${field_nombre}">Nombre</label>
<input type="text" id="${field_nombre}" style="flex: 1;" />
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
<label for="${field_unidad}">Unidad</label>
<select id="${field_unidad}" style="flex: 1;">
<option value="unidad(es)">unidad(es)</option>
<option value="paquete(s)">paquete(s)</option>
<option value="caja(s)">caja(s)</option>
<option value="rollo(s)">rollo(s)</option>
<option value="bote(s)">bote(s)</option>
<option value="metro(s)">metro(s)</option>
<option value="litro(s)">litro(s)</option>
<option value="kg">kg</option>
</select>
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
<label for="${field_cantidad}">Cantidad Actual</label>
<input type="number" step="0.5" id="${field_cantidad}" style="flex: 1;" disabled />
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
<label for="${field_cantidad_min}">Cantidad Minima</label>
<input type="number" step="0.5" id="${field_cantidad_min}" style="flex: 1;" />
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
<label for="${field_ubicacion}">Ubicación</label>
<input
type="text"
id="${field_ubicacion}"
value="-"
list="${field_ubicacion}_list"
style="flex: 1;"
/>
<datalist id="${field_ubicacion}_list"></datalist>
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 100%;">
<label for="${field_notas}">Notas</label>
<textarea id="${field_notas}" style="flex: 1;"></textarea>
</div>
</div>
<div
id="${mov_modal}"
style="display: none;position: fixed;z-index: 9999;left: 0;top: 0;width: 100%;height: 100%;overflow: auto;background: rgba(0,0,0,0.45);"
>
<div
style="background: #fff;margin: 2vh auto;padding: 14px;border: 1px solid #888;width: min(960px, 96vw);max-height: 94vh;overflow: auto;border-radius: 8px;box-sizing: border-box;"
>
<div style="display: flex;justify-content: space-between;align-items: center;gap: 10px;">
<h3 style="margin: 0;">Realizar movimiento</h3>
<button type="button" id="${mov_modal_close}" class="rojo">Cerrar</button>
</div>
<div style="margin-top: 12px;display: flex;gap: 10px;align-items: flex-end;flex-wrap: wrap;">
<div style="display: flex;flex-wrap: wrap;gap: 10px 12px;align-items: flex-end;flex: 1 1 420px;">
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
<label for="${mov_tipo}">Tipo</label>
<select id="${mov_tipo}" style="flex: 1;">
<option value="Entrada">Entrada</option>
<option value="Salida">Salida</option>
<option value="Ajuste">Ajuste</option>
</select>
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
<label for="${mov_cantidad}">Cantidad</label>
<input type="number" step="0.5" id="${mov_cantidad}" style="flex: 1;" />
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
<label for="${mov_nota}">Nota</label>
<input type="text" id="${mov_nota}" style="flex: 1;" placeholder="Motivo del movimiento" />
</div>
</div>
<div style="display: flex;justify-content: flex-end;flex: 1 1 120px;min-width: 120px;">
<button type="button" class="saveico" id="${mov_btn}">
<img src="static/floppy_disk_green.png" />
<br>Guardar
</button>
</div>
</div>
<h4 style="margin: 14px 0 6px 0;">Registro de movimientos</h4>
<div id="${mov_registro}"></div>
</div>
</div>
<option value="metro(s)">metro(s)</option>
<option value="litro(s)">litro(s)</option>
<option value="kg">kg</option></select
><br /><br />
</label>
<label>
Cantidad Actual<br />
<input type="number" step="0.5" id="${field_cantidad}" /><br /><br />
</label>
<label>
Cantidad Minima<br />
<input type="number" step="0.5" id="${field_cantidad_min}" /><br /><br />
</label>
<label>
Ubicación<br />
<input
type="text"
id="${field_ubicacion}"
value="-"
list="${field_ubicacion}_list"
/><br /><br />
<!-- Autocompletar con ubicaciones existentes -->
<datalist id="${field_ubicacion}_list"></datalist>
</label>
<label>
Notas<br />
<textarea id="${field_notas}"></textarea><br /><br />
</label>
<hr />
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
<button class="saveico" id="${btn_guardar}">
<img src="static/floppy_disk_green.png" />
<br>Guardar
</button>
<button class="delico" id="${btn_borrar}">
<img src="static/garbage.png" />
<br>Borrar
</button>
<button class="opicon" id="${mov_open_modal_btn}">
<img src="static/exchange.png" />
<br>Movimientos
</button>
<button class="opicon" id="${btn_print_chart}">
<img src="static/printer2.png" />
<br>Imprimir
</button>
<button class="opicon" onclick="setUrlHash('materiales')">
<img src="static/exit.png" />
<br>Salir
</button>
</fieldset>
<div id="${mov_chart}" style="max-width: 980px;width: 100%;margin-top: 14px;min-height: 260px;height: min(400px, 52vh);"></div>
`;
// Cargar ubicaciones existentes para autocompletar
DB.map('materiales', (data) => {
@@ -125,6 +381,9 @@ PAGES.materiales = {
document.getElementById(field_ubicacion).value = data['Ubicacion'] || '-';
document.getElementById(field_revision).value = data['Revision'] || '-';
document.getElementById(field_notas).value = data['Notas'] || '';
movimientos = Array.isArray(data['Movimientos']) ? data['Movimientos'] : [];
renderMovimientos();
renderMovimientosChart();
}
if (typeof data == 'string') {
TS_decrypt(
@@ -140,6 +399,83 @@ PAGES.materiales = {
load_data(data || {});
}
});
document.getElementById(mov_open_modal_btn).onclick = () => {
document.getElementById(mov_modal).style.display = 'block';
renderMovimientos();
};
document.getElementById(btn_print_chart).onclick = () => {
window.print();
};
document.getElementById(mov_modal_close).onclick = () => {
document.getElementById(mov_modal).style.display = 'none';
};
document.getElementById(mov_modal).onclick = (evt) => {
if (evt.target.id === mov_modal) {
document.getElementById(mov_modal).style.display = 'none';
}
};
document.getElementById(mov_btn).onclick = () => {
var btn = document.getElementById(mov_btn);
if (btn.disabled) return;
var tipo = document.getElementById(mov_tipo).value;
var cantidadMov = parseNum(document.getElementById(mov_cantidad).value, NaN);
var nota = document.getElementById(mov_nota).value || '';
var actual = parseNum(document.getElementById(field_cantidad).value, 0);
if (!Number.isFinite(cantidadMov) || cantidadMov <= 0) {
toastr.warning('Indica una cantidad válida para el movimiento');
return;
}
var nuevaCantidad = actual;
if (tipo === 'Entrada') {
nuevaCantidad = actual + cantidadMov;
} else if (tipo === 'Salida') {
nuevaCantidad = actual - cantidadMov;
} else if (tipo === 'Ajuste') {
nuevaCantidad = cantidadMov;
}
movimientos.unshift({
Fecha: new Date().toISOString(),
Tipo: tipo,
Cantidad: cantidadMov,
Antes: actual,
Despues: nuevaCantidad,
Nota: nota,
});
document.getElementById(field_cantidad).value = nuevaCantidad;
document.getElementById(field_revision).value = FECHA_ISO;
document.getElementById(mov_cantidad).value = '';
document.getElementById(mov_nota).value = '';
renderMovimientos();
renderMovimientosChart();
btn.disabled = true;
btn.style.opacity = '0.5';
document.getElementById('actionStatus').style.display = 'block';
DB.put('materiales', mid, buildMaterialData())
.then(() => {
toastr.success('Movimiento registrado');
})
.catch((e) => {
console.warn('DB.put error', e);
toastr.error('Error al guardar el movimiento');
})
.finally(() => {
btn.disabled = false;
btn.style.opacity = '1';
document.getElementById('actionStatus').style.display = 'none';
});
};
document.getElementById(btn_guardar).onclick = () => {
// Disable button to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
@@ -148,15 +484,7 @@ PAGES.materiales = {
guardarBtn.disabled = true;
guardarBtn.style.opacity = '0.5';
var data = {
Nombre: document.getElementById(field_nombre).value,
Unidad: document.getElementById(field_unidad).value,
Cantidad: document.getElementById(field_cantidad).value,
Cantidad_Minima: document.getElementById(field_cantidad_min).value,
Ubicacion: document.getElementById(field_ubicacion).value,
Revision: document.getElementById(field_revision).value,
Notas: document.getElementById(field_notas).value,
};
var data = buildMaterialData();
document.getElementById('actionStatus').style.display = 'block';
DB.put('materiales', mid, data)
.then(() => {

View File

@@ -48,8 +48,14 @@ PAGES.mensajes = {
<div id="${attachments_list}"></div>
</label>
<hr />
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
<button class="saveico" id="${btn_guardar}">
<img src="static/floppy_disk_green.png" />
<br>Guardar
</button>
<button class="delico" id="${btn_borrar}">
<img src="static/garbage.png" />
<br>Borrar
</button>
</fieldset>
`;
DB.get('mensajes', mid).then((data) => {

View File

@@ -45,8 +45,14 @@ PAGES.notas = {
<div id="${attachments_list}"></div>
</label>
<hr />
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
<button class="saveico" id="${btn_guardar}">
<img src="static/floppy_disk_green.png" />
<br>Guardar
</button>
<button class="delico" id="${btn_borrar}">
<img src="static/garbage.png" />
<br>Borrar
</button>
</fieldset>
`;
var divact = document.getElementById(div_actions);

View File

@@ -87,8 +87,14 @@ PAGES.personas = {
Notas<br>
<textarea id="${field_notas}"></textarea><br><br>
</label><hr>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
<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 resized = '';