diff --git a/src/index.html b/src/index.html index 4701760..59e2f53 100644 --- a/src/index.html +++ b/src/index.html @@ -89,6 +89,7 @@ + \ No newline at end of file diff --git a/src/page/cajas.js b/src/page/cajas.js new file mode 100644 index 0000000..4470c6d --- /dev/null +++ b/src/page/cajas.js @@ -0,0 +1,760 @@ +PERMS['cajas'] = 'Cajas'; +PERMS['cajas:edit'] = '> Editar'; +PAGES.cajas = { + navcss: 'btn8', + icon: 'static/appico/credit_cards.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(); + + container.innerHTML = html` +

Movimiento de Caja

+
+ + + + + + +
+ + +
+ `; + + // Load transaction data + DB.get('cajas_movimientos', movimientoId).then((data) => { + function load_data(data) { + if (!data) return; + + // 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 + DB.getAttachment('cajas_movimientos', movimientoId, 'foto') + .then((durl) => { + if (durl) { + document.getElementById(render_foto).src = durl; + } else { + document.getElementById(render_foto).style.display = 'none'; + } + }) + .catch(() => { + document.getElementById(render_foto).style.display = 'none'; + }); + } + + if (typeof data === 'string') { + TS_decrypt( + data, + SECRET, + (data, wasEncrypted) => { + load_data(data); + }, + 'cajas_movimientos', + movimientoId + ); + } else { + load_data(data || {}); + } + }); + + document.getElementById(btn_volver).onclick = () => { + setUrlHash('cajas,' + cajaId); + }; + + // Show delete button only if user has edit permission + if (checkRole('cajas:edit')) { + document.getElementById(btn_borrar).style.display = 'inline-block'; + 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(','); + if (parts[1] === 'nuevo_movimiento' && parts[2]) { + PAGES.cajas.nuevo_movimiento(parts[2]); + return; + } + if (parts[1] === 'movimiento' && parts[2] && parts[3]) { + PAGES.cajas.movimiento(parts[2], 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,nuevo_movimiento,' + mid); + }; + } 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_movimientos', + config, + 'cajas_movimientos', + document.getElementById(movimientos_container), + function (data, new_tr) { + new_tr.onclick = () => { + setUrlHash('cajas,movimiento,' + mid + ',' + 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('')); + }; + } + }, +};