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:
co-authored by
Claude Sonnet 4.6
parent
b9bb4fc628
commit
a7aac706ee
@@ -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)));
|
||||
|
||||
Reference in New Issue
Block a user