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(); 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(','); 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('')); }; } }, };