362 lines
13 KiB
Plaintext

<% /* Compras / Gastos */ %>
<div class="container-fluid py-3">
<div class="d-flex align-items-center mb-2">
<h3 class="mb-0">Compras / Gastos</h3>
<div class="ms-auto d-flex gap-2">
<button id="btnNueva" class="btn btn-outline-secondary btn-sm">Nueva</button>
<span id="status" class="small text-muted">—</span>
</div>
</div>
<!-- Formulario -->
<div class="card shadow-sm mb-3">
<div class="card-header"><strong id="formTitle">Nueva compra</strong></div>
<div class="card-body">
<form id="frmCompra" class="row g-3">
<input type="hidden" id="id_compra" value="">
<div class="col-12 col-md-5">
<label class="form-label">Proveedor</label>
<select id="id_proveedor" class="form-select" required></select>
</div>
<div class="col-12 col-md-3">
<label class="form-label">Fecha</label>
<input id="fec_compra" type="datetime-local" class="form-control" required>
</div>
<div class="col-12 col-md-4">
<label class="form-label">Total</label>
<input id="total" type="text" class="form-control" value="$ 0" disabled>
</div>
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="fw-semibold">Renglones</div>
<div>
<button type="button" id="addRow" class="btn btn-sm btn-outline-primary">Agregar renglón</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle" id="tblDet">
<thead class="table-light">
<tr>
<th style="width:110px">Tipo</th>
<th>Ítem</th>
<th style="width:140px" class="text-end">Cantidad</th>
<th style="width:160px" class="text-end">Precio</th>
<th style="width:140px" class="text-end">Subtotal</th>
<th style="width:60px"></th>
</tr>
</thead>
<tbody>
<tr class="empty">
<td colspan="6" class="p-3 text-muted">Sin renglones</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-12 d-flex gap-2">
<button id="btnGuardar" type="submit" class="btn btn-success">Guardar</button>
<button id="btnEliminar" type="button" class="btn btn-outline-danger d-none">Eliminar</button>
</div>
</form>
</div>
</div>
<!-- Listado -->
<div class="card shadow-sm">
<div class="card-header d-flex align-items-center">
<strong>Compras recientes</strong>
<input id="buscar" class="form-control form-control-sm ms-auto" style="max-width:260px" placeholder="Buscar proveedor…">
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm align-middle mb-0" id="tblCompras">
<thead class="table-light">
<tr>
<th>#</th>
<th>Proveedor</th>
<th>Fecha</th>
<th class="text-end">Total</th>
<th></th>
</tr>
</thead>
<tbody>
<tr><td colspan="5" class="p-3 text-muted">Sin datos</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<style>
#tblDet select, #tblDet input { min-height: 34px; }
.money { text-align: right; }
</style>
<script>
const $ = s => document.querySelector(s);
const $$ = (s, root=document) => Array.from(root.querySelectorAll(s));
const z2 = n => String(n).padStart(2,'0');
const parseNum = v => (typeof v==='number') ? v : Number(String(v).replace(/[^\d.,-]/g,'').replace('.','').replace(',','.')) || 0;
function fmtMoneyInt(v, mode = 'round') {
const n = Number(v || 0);
const i = mode === 'trunc' ? Math.trunc(n) : mode === 'floor' ? Math.floor(n) : Math.round(n);
return '$ ' + i.toLocaleString('es-UY', { maximumFractionDigits: 0 });
}
const onlyDigits = s => String(s ?? '').replace(/\D+/g, '');
function wireIntInput(input, onChange) {
const sync = () => {
const n = Number(onlyDigits(input.value) || '0'); // entero
input.dataset.raw = String(n); // guardo valor crudo
input.value = n.toLocaleString('es-UY'); // muestro con miles
if (onChange) onChange(n);
};
input.addEventListener('input', () => setTimeout(sync, 0));
input.addEventListener('blur', sync);
// 1a sync
sync();
}
function getIntInput(input) {
const s = input?.dataset?.raw ?? onlyDigits(input?.value);
return Number(s || '0');
}
async function jget(u){ const r=await fetch(u); const j=await r.json().catch(()=>null); if(!r.ok) throw new Error(j?.error||r.statusText); return j; }
async function jpost(u,b){ const r=await fetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})}); const j=await r.json().catch(()=>null); if(!r.ok) throw new Error(j?.error||r.statusText); return j; }
// Catálogos
let CATS = { prov:[], mat:[], prod:[] };
async function loadCatalogos(){
$('#status').textContent = 'Cargando catálogos…';
const [prov, mat, prod] = await Promise.all([
jget('/api/table/proveedores?limit=10000'),
jget('/api/table/mate_primas?limit=10000'),
jget('/api/table/productos?limit=10000')
]);
CATS.prov = prov||[]; CATS.mat = mat||[]; CATS.prod = prod||[];
const sel = $('#id_proveedor'); sel.innerHTML = '<option value="">—</option>' + CATS.prov.map(p=>`<option value="${p.id_proveedor}">${p.raz_social||p.nombre||('Prov#'+p.id_proveedor)}</option>`).join('');
$('#status').textContent = 'Listo';
}
// Renglón
function addRow(data){
const tb = $('#tblDet tbody');
tb.querySelector('.empty')?.remove();
const tr = document.createElement('tr');
const tipo = data?.tipo || 'MAT'; // MAT | PROD
const id = data?.id || '';
const cant = data?.cantidad != null ? data.cantidad : 1;
const pu = data?.precio != null ? data.precio : 0;
tr.innerHTML = `
<td>
<select class="form-select form-select-sm tipo">
<option value="MAT"${tipo==='MAT'?' selected':''}>Materia</option>
<option value="PROD"${tipo==='PROD'?' selected':''}>Producto</option>
</select>
</td>
<td>
<select class="form-select form-select-sm item"></select>
</td>
<td><input type="text" inputmode="numeric" pattern="[0-9]*"
class="form-control form-control-sm text-end qty" value="${cant}"></td>
<td><input type="text" inputmode="numeric" pattern="[0-9]*"
class="form-control form-control-sm text-end price" value="${pu}"></td>
<td class="text-end sub">$ 0</td>
<td><button type="button" class="btn btn-sm btn-outline-danger del">✕</button></td>
`;
tb.appendChild(tr);
// load items segun tipo
function fillItems(selTipo, selItem, selectedId){
const list = selTipo.value === 'MAT' ? CATS.mat : CATS.prod;
selItem.innerHTML = '<option value="">—</option>' + list.map(i => {
const id = selTipo.value === 'MAT' ? i.id_mat_prima : i.id_producto;
const nm = i.nombre || ('#'+id);
return `<option value="${id}">${nm}</option>`;
}).join('');
if (selectedId) selItem.value = selectedId;
}
const selTipo = tr.querySelector('.tipo');
const selItem = tr.querySelector('.item');
const qty = tr.querySelector('.qty');
const price = tr.querySelector('.price');
const subCell = tr.querySelector('.sub');
selTipo.addEventListener('change', ()=>{ fillItems(selTipo, selItem, null); updateRow(); });
[selItem, qty, price].forEach(el => el.addEventListener('input', updateRow));
tr.querySelector('.del').addEventListener('click', ()=>{ tr.remove(); recalcTotal(); if (!tb.children.length) tb.innerHTML='<tr class="empty"><td colspan="6" class="p-3 text-muted">Sin renglones</td></tr>'; });
fillItems(selTipo, selItem, id);
function updateRow(){
const s = getIntInput(qty) * getIntInput(price);
subCell.textContent = fmtMoneyInt(s);
recalcTotal();
}
wireIntInput(qty, updateRow);
wireIntInput(price, updateRow);
updateRow();
}
function recalcTotal(){
let tot = 0;
$('#tblDet tbody').querySelectorAll('tr').forEach(tr=>{
if (tr.classList.contains('empty')) return;
const q = getIntInput(tr.querySelector('.qty'));
const p = getIntInput(tr.querySelector('.price'));
tot += q * p;
});
$('#total').value = fmtMoneyInt(tot);
return tot;
}
function readFormToPayload(){
const id_compra = $('#id_compra').value ? Number($('#id_compra').value) : null;
const id_proveedor = Number($('#id_proveedor').value || 0);
const fec_compra = $('#fec_compra').value
? new Date($('#fec_compra').value).toISOString().slice(0,19).replace('T',' ')
: null;
const det = [];
// 👇 OJO: iteramos sobre TODAS las filas reales
$('#tblDet tbody').querySelectorAll('tr').forEach(tr=>{
if (tr.classList.contains('empty')) return;
const tipo = tr.querySelector('.tipo').value; // 'MAT' | 'PROD'
const id = Number(tr.querySelector('.item').value||0);
const qty = getIntInput(tr.querySelector('.qty')); // entero
const pu = getIntInput(tr.querySelector('.price')); // entero
if (id && qty>0 && pu>=0) det.push({ tipo, id, cantidad: qty, precio: pu });
});
return { id_compra, id_proveedor, fec_compra, detalles: det };
}
// Guardar / Eliminar
async function saveCompra(){
const payload = readFormToPayload();
if (!payload.id_proveedor) { alert('Seleccioná un proveedor.'); return; }
if (!payload.fec_compra) { alert('Indicá la fecha.'); return; }
if (!payload.detalles.length){ alert('Agregá al menos un renglón.'); return; }
$('#btnGuardar').disabled = true; $('#status').textContent = 'Guardando…';
try{
const res = await jpost('/api/rpc/save_compra', payload);
$('#id_compra').value = res.id_compra;
$('#btnEliminar').classList.remove('d-none');
$('#formTitle').textContent = 'Editar compra #' + res.id_compra;
await loadListado();
alert('Compra guardada.');
}catch(e){
alert('Error al guardar: ' + e.message);
}finally{
$('#btnGuardar').disabled = false; $('#status').textContent = 'Listo';
}
}
async function deleteCompra(){
const id = Number($('#id_compra').value||0);
if (!id) return;
if (!confirm('¿Eliminar compra #' + id + '?')) return;
$('#btnEliminar').disabled = true;
try{
await jpost('/api/rpc/delete_compra', { id_compra: id });
nuevaCompra();
await loadListado();
}catch(e){
alert('Error al eliminar: '+e.message);
}finally{
$('#btnEliminar').disabled = false;
}
}
function nuevaCompra(){
$('#formTitle').textContent = 'Nueva compra';
$('#id_compra').value = '';
$('#id_proveedor').value = '';
$('#fec_compra').value = new Date().toISOString().slice(0,16);
$('#total').value = '$ 0';
$('#btnEliminar').classList.add('d-none');
const tb = $('#tblDet tbody'); tb.innerHTML = '<tr class="empty"><td colspan="6" class="p-3 text-muted">Sin renglones</td></tr>';
}
async function cargarCompra(id){
$('#status').textContent = 'Cargando compra…';
try{
const data = await jpost('/api/rpc/get_compra', { id_compra: id });
$('#id_compra').value = data.id_compra;
$('#id_proveedor').value = data.id_proveedor;
$('#fec_compra').value = (data.fec_compra || '').replace(' ', 'T').slice(0,16);
const tb = $('#tblDet tbody'); tb.innerHTML='';
(data.detalles||[]).forEach(d => addRow(d));
recalcTotal();
$('#btnEliminar').classList.remove('d-none');
$('#formTitle').textContent = 'Editar compra #' + id;
} catch(e){
alert('No se pudo cargar: ' + e.message);
} finally {
$('#status').textContent = 'Listo';
}
}
// Listado
async function loadListado(){
// Recomendado: vista vw_compras (más abajo)
const rows = await jget('/api/table/vw_compras?limit=200&order_by=fec_compra%20desc');
const tb = $('#tblCompras tbody');
if (!rows?.length){ tb.innerHTML = '<tr><td colspan="5" class="p-3 text-muted">Sin datos</td></tr>'; return; }
tb.innerHTML = '';
rows.forEach(r=>{
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${r.id_compra}</td>
<td>${r.proveedor}</td>
<td>${(r.fec_compra||'').replace('T',' ').slice(0,16)}</td>
<td class="text-end">${fmtMoneyInt(r.total)}</td>
<td class="text-end"><button class="btn btn-sm btn-outline-primary ver" data-id="${r.id_compra}">Ver/Editar</button></td>`;
tb.appendChild(tr);
});
$('#buscar').addEventListener('input', (e)=>{
const q = e.target.value.toLowerCase();
tb.querySelectorAll('tr').forEach(tr=>{
const prov = tr.children[1]?.textContent.toLowerCase() || '';
tr.style.display = prov.includes(q) ? '' : 'none';
});
});
tb.addEventListener('click', (ev)=>{
const btn = ev.target.closest('button.ver');
if (!btn) return;
const id = Number(btn.dataset.id);
cargarCompra(id);
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
// Eventos
document.getElementById('addRow').addEventListener('click', ()=> addRow());
document.getElementById('frmCompra').addEventListener('submit', (ev)=>{ ev.preventDefault(); saveCompra(); });
document.getElementById('btnEliminar').addEventListener('click', deleteCompra);
document.getElementById('btnNueva').addEventListener('click', nuevaCompra);
// Init
(async function init(){
await loadCatalogos();
nuevaCompra();
await loadListado();
})();
</script>