feat(recepcion): motor de precios + selector empresa/convenio

- Nueva sección empresa/convenio en recepcion.php: búsqueda live por
  nombre/NIT, selector de subgrupo, campo autorización (cuando req_autoriza),
  diagnóstico CIE-10
- Tabla de precios calculados en tiempo real: resolución por tarifa de la
  empresa o subgrupo, descuento empresa aplicado, botón "Aplicar al cobro"
- Al agregar/quitar exámenes o cambiar empresa/subgrupo se recalcula automáticamente
- Nuevo API get_precios_examenes.php: resuelve precios desde lab_tarifas
  con soporte de tarifas derivadas por porcentaje
- create_solicitud.php guarda nit_empresa, subgrupo_id, autorizacion,
  diag_ppal, items_precio (snapshot JSON al momento de la recepción)
- Migración 10: ALTER turnero_solicitudes añade los 5 campos nuevos

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-04 22:09:26 -05:00
co-authored by Claude Sonnet 4.6
parent b9bb4fc628
commit a7aac706ee
4 changed files with 423 additions and 4 deletions
+245
View File
@@ -608,6 +608,80 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
</div>
<!-- ── Sección 3.5: Empresa / Convenio ── -->
<div class="ficha-section" id="sec-empresa">
<h6><i class="fas fa-building me-1"></i>Empresa / Convenio <span class="text-muted fw-normal">(opcional)</span></h6>
<!-- Buscador empresa -->
<div id="empresa-seleccionada" class="d-none mb-2">
<div class="d-flex align-items-center gap-2 p-2 rounded border" style="background:#f0f9ff">
<div class="flex-grow-1">
<div class="fw-semibold small" id="empresa-badge-nombre"></div>
<div class="text-muted" style="font-size:.75rem" id="empresa-badge-meta"></div>
</div>
<button type="button" class="btn btn-link btn-sm p-0 text-danger" onclick="quitarEmpresa()" title="Quitar empresa">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div id="empresa-buscador">
<input type="text" id="inp-buscar-empresa" class="form-control form-control-sm"
placeholder="Buscar empresa por nombre o NIT…"
autocomplete="off" oninput="buscarEmpresa()">
<div class="position-relative">
<ul id="empresa-resultados" class="list-unstyled mb-0 border rounded bg-white shadow-sm position-absolute w-100 d-none"
style="z-index:200;max-height:200px;overflow-y:auto;top:2px"></ul>
</div>
</div>
<input type="hidden" id="inp-empresa-nit">
<!-- Subgrupo (se muestra cuando hay subgrupos) -->
<div id="wrap-subgrupo" class="d-none mt-2">
<label class="form-label mb-1 small fw-semibold">Subgrupo</label>
<select id="sel-subgrupo" class="form-select form-select-sm" onchange="recalcularPrecios()">
<option value="">— Sin subgrupo —</option>
</select>
</div>
<!-- Autorización -->
<div id="wrap-autorizacion" class="d-none mt-2">
<label class="form-label mb-1 small fw-semibold text-warning">
<i class="fas fa-key me-1"></i>N.° Autorización <span class="text-danger">*</span>
</label>
<input type="text" id="inp-autorizacion" class="form-control form-control-sm"
placeholder="Número de autorización EPS" maxlength="100">
</div>
<!-- Diagnóstico -->
<div id="wrap-diag" class="d-none mt-2">
<label class="form-label mb-1 small fw-semibold">Diagnóstico (CIE-10)</label>
<input type="text" id="inp-diag" class="form-control form-control-sm"
placeholder="Código CIE-10 diagnóstico principal" maxlength="20">
</div>
<!-- Tabla de precios calculados -->
<div id="panel-precios" class="d-none mt-3">
<div class="d-flex align-items-center justify-content-between mb-1">
<span class="small fw-semibold text-muted text-uppercase" style="font-size:.7rem;letter-spacing:.06em">
Precios
</span>
<span class="badge bg-light text-dark border small" id="precio-tarifa-label"></span>
</div>
<div id="tabla-precios" class="mb-1" style="font-size:.82rem"></div>
<div class="d-flex justify-content-between align-items-center pt-1 border-top">
<span class="fw-bold small">Total calculado</span>
<span class="fw-bold text-success" id="precio-total-calc">$0</span>
</div>
<div id="precio-dcto-row" class="d-none d-flex justify-content-between text-muted" style="font-size:.78rem">
<span>Descuento empresa (<span id="precio-dcto-pct"></span>%)</span>
<span id="precio-dcto-val" class="text-danger"></span>
</div>
<button type="button" class="btn btn-outline-primary btn-sm w-100 mt-2"
onclick="aplicarPrecioCalculado()" id="btn-aplicar-precio">
<i class="fas fa-check me-1"></i>Aplicar al cobro
</button>
</div>
</div>
<!-- ── Sección 4: Pago ── -->
<div class="ficha-section">
<h6><i class="fas fa-dollar-sign me-1"></i>Cobro (opcional)</h6>
@@ -904,6 +978,8 @@ document.addEventListener('DOMContentLoaded', () => {
examTS = new TomSelect('#sel-examenes', {
plugins: ['remove_button'],
maxOptions: null,
onItemAdd: () => recalcularPrecios(),
onItemRemove: () => recalcularPrecios(),
render: {
option(data, escape) {
const consent = data['data-consent'] == '1' || data.consent == '1';
@@ -1483,6 +1559,11 @@ async function guardarSolicitud() {
embarazada: document.getElementById('chk-embarazada').checked ? 1 : 0,
solo_muestras: soloMuestras ? 1 : 0,
medico_id: parseInt(document.getElementById('inp-medico-id').value) || null,
nit_empresa: document.getElementById('inp-empresa-nit').value.trim() || null,
subgrupo_id: parseInt(document.getElementById('sel-subgrupo').value) || null,
autorizacion: document.getElementById('inp-autorizacion').value.trim() || null,
diag_ppal: document.getElementById('inp-diag').value.trim() || null,
items_precio: _preciosActivos.length ? _preciosActivos : null,
}),
});
const json = await res.json();
@@ -1890,8 +1971,172 @@ function resetCheckboxes() {
document.getElementById('inp-obs').value = '';
resetPagoCombinado();
resetMedico();
quitarEmpresa();
}
// ── Empresa / Convenio ────────────────────────────────────────
let _empresaActiva = null;
let _empresaBusqTimer;
function buscarEmpresa() {
clearTimeout(_empresaBusqTimer);
_empresaBusqTimer = setTimeout(_doEmpresaSearch, 280);
}
async function _doEmpresaSearch() {
const q = document.getElementById('inp-buscar-empresa').value.trim();
const ul = document.getElementById('empresa-resultados');
if (q.length < 2) { ul.classList.add('d-none'); return; }
const res = await fetch(`${BASE_URL_API_LAB}empresas.php?action=list&search=${encodeURIComponent(q)}&activa=1&limit=10`);
const j = await res.json();
const list = j.empresas || [];
if (!list.length) { ul.innerHTML = '<li class="px-3 py-2 text-muted small">Sin resultados</li>'; ul.classList.remove('d-none'); return; }
ul.innerHTML = list.map(e => `
<li class="px-3 py-2 cursor-pointer hover-bg"
style="cursor:pointer;border-bottom:1px solid #f1f5f9"
onmousedown="seleccionarEmpresa(${JSON.stringify(e)})">
<div class="fw-semibold small">${escHtml(e.nombre)}</div>
<div class="text-muted" style="font-size:.75rem">NIT ${escHtml(e.nit)}${e.tarifa_nombre ? ' · ' + escHtml(e.tarifa_nombre) : ''}</div>
</li>
`).join('');
ul.classList.remove('d-none');
}
async function seleccionarEmpresa(e) {
document.getElementById('empresa-resultados').classList.add('d-none');
document.getElementById('empresa-buscador').classList.add('d-none');
document.getElementById('empresa-seleccionada').classList.remove('d-none');
document.getElementById('empresa-badge-nombre').textContent = e.nombre;
document.getElementById('empresa-badge-meta').textContent =
`NIT ${e.nit}${e.tarifa_nombre ? ' · ' + e.tarifa_nombre : ''}${parseFloat(e.descuento_pct) > 0 ? ' · Dcto: ' + e.descuento_pct + '%' : ''}`;
document.getElementById('inp-empresa-nit').value = e.nit;
_empresaActiva = e;
// Mostrar autorización y diagnóstico si la empresa los exige
document.getElementById('wrap-autorizacion').classList.toggle('d-none', !parseInt(e.req_autoriza));
document.getElementById('wrap-diag').classList.remove('d-none');
// Cargar subgrupos
const res = await fetch(`${BASE_URL_API_LAB}empresa_subgrupos.php?nit_empresa=${encodeURIComponent(e.nit)}`);
const j = await res.json();
const subs = j.subgrupos || [];
const wrapSub = document.getElementById('wrap-subgrupo');
const selSub = document.getElementById('sel-subgrupo');
if (subs.length) {
selSub.innerHTML = '<option value="">— Sin subgrupo —</option>' +
subs.map(s => `<option value="${s.id}">${escHtml(s.subgrupo)}${s.tarifa_nombre ? ' · ' + escHtml(s.tarifa_nombre) : ''}</option>`).join('');
wrapSub.classList.remove('d-none');
} else {
selSub.innerHTML = '<option value="">— Sin subgrupo —</option>';
wrapSub.classList.add('d-none');
}
// Si es EPS, preseleccionar forma de pago
if (document.getElementById('sel-pago').value === '') {
document.getElementById('sel-pago').value = 'eps';
togglePagoCombinado();
}
recalcularPrecios();
}
function quitarEmpresa() {
_empresaActiva = null;
document.getElementById('inp-empresa-nit').value = '';
document.getElementById('inp-buscar-empresa').value = '';
document.getElementById('empresa-seleccionada').classList.add('d-none');
document.getElementById('empresa-buscador').classList.remove('d-none');
document.getElementById('empresa-resultados').classList.add('d-none');
document.getElementById('wrap-subgrupo').classList.add('d-none');
document.getElementById('wrap-autorizacion').classList.add('d-none');
document.getElementById('wrap-diag').classList.add('d-none');
document.getElementById('panel-precios').classList.add('d-none');
document.getElementById('sel-subgrupo').innerHTML = '<option value="">— Sin subgrupo —</option>';
document.getElementById('inp-autorizacion').value = '';
document.getElementById('inp-diag').value = '';
}
// ── Motor de precios ──────────────────────────────────────────
let _recalcTimer;
let _preciosActivos = [];
function recalcularPrecios() {
clearTimeout(_recalcTimer);
_recalcTimer = setTimeout(_doRecalcular, 200);
}
async function _doRecalcular() {
const examIds = examTS ? examTS.getValue() : [];
const nit = document.getElementById('inp-empresa-nit').value.trim();
const subId = document.getElementById('sel-subgrupo').value;
if (!examIds.length || !nit) {
document.getElementById('panel-precios').classList.add('d-none');
return;
}
const params = new URLSearchParams({ nit_empresa: nit });
examIds.forEach(id => params.append('exam_ids[]', id));
if (subId) params.append('subgrupo_id', subId);
const res = await fetch(`${API}get_precios_examenes.php?${params}`);
const j = await res.json();
if (!j.ok) return;
_preciosActivos = j.items || [];
// Tabla
const tabla = document.getElementById('tabla-precios');
const tienePrecios = j.items.some(i => i.tiene_precio);
if (!tienePrecios) {
tabla.innerHTML = '<div class="text-muted small text-center py-1">Sin precios configurados para esta tarifa</div>';
} else {
tabla.innerHTML = j.items.map(i => `
<div class="d-flex justify-content-between py-1" style="border-bottom:1px solid #f1f5f9">
<span class="text-truncate me-2" style="max-width:200px" title="${escHtml(i.nombre)}">${escHtml(i.nombre)}</span>
<span class="${i.tiene_precio ? 'fw-semibold' : 'text-muted'}">
${i.tiene_precio ? '$' + fmt(i.valor_final) : 'Sin precio'}
</span>
</div>
`).join('');
}
// Totales
document.getElementById('precio-tarifa-label').textContent = j.tarifa_nombre || '';
document.getElementById('precio-total-calc').textContent = '$' + fmt(j.subtotal);
const dctoRow = document.getElementById('precio-dcto-row');
if (j.descuento_pct > 0) {
document.getElementById('precio-dcto-pct').textContent = j.descuento_pct;
document.getElementById('precio-dcto-val').textContent = '-$' + fmt(j.descuento_val);
document.getElementById('precio-total-calc').textContent = '$' + fmt(j.total);
dctoRow.classList.remove('d-none');
} else {
dctoRow.classList.add('d-none');
}
document.getElementById('panel-precios').classList.remove('d-none');
}
function aplicarPrecioCalculado() {
const total = _preciosActivos.reduce((sum, i) => sum + (i.valor_final || 0), 0);
const nit = document.getElementById('inp-empresa-nit').value.trim();
const emp = _empresaActiva;
const dcto = parseFloat(emp?.descuento_pct || 0);
const final = Math.round(total * (1 - dcto / 100));
document.getElementById('inp-total').value = final || '';
if (emp && !document.getElementById('sel-pago').value) {
document.getElementById('sel-pago').value = 'eps';
togglePagoCombinado();
}
mostrarToast('Total aplicado: $' + fmt(final), 'ok', 2500);
}
function fmt(n) {
return Math.round(n || 0).toLocaleString('es-CO');
}
const BASE_URL_API_LAB = '<?= BASE_URL ?>/api/lab/';
function escHtml(str) {
const d = document.createElement('div');
d.appendChild(document.createTextNode(String(str)));