From a7aac706ee85bb37ffd68b8770910bff3ec45fc7 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Sat, 4 Jul 2026 22:09:26 -0500 Subject: [PATCH] feat(recepcion): motor de precios + selector empresa/convenio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../20260704_lis_10_solicitud_empresa.sql | 16 ++ modules/turnero/api/create_solicitud.php | 17 +- modules/turnero/api/get_precios_examenes.php | 149 +++++++++++ modules/turnero/views/recepcion.php | 245 ++++++++++++++++++ 4 files changed, 423 insertions(+), 4 deletions(-) create mode 100644 migrations/20260704_lis_10_solicitud_empresa.sql create mode 100644 modules/turnero/api/get_precios_examenes.php diff --git a/migrations/20260704_lis_10_solicitud_empresa.sql b/migrations/20260704_lis_10_solicitud_empresa.sql new file mode 100644 index 0000000..4c7e9b3 --- /dev/null +++ b/migrations/20260704_lis_10_solicitud_empresa.sql @@ -0,0 +1,16 @@ +-- ============================================================= +-- LIS 10 — Extiende turnero_solicitudes con campos de empresa/convenio +-- Todos DEFAULT NULL para no romper datos existentes. +-- ============================================================= + +ALTER TABLE turnero_solicitudes + ADD COLUMN IF NOT EXISTS nit_empresa VARCHAR(20) DEFAULT NULL + COMMENT 'FK lab_empresas.nit — empresa/EPS del paciente', + ADD COLUMN IF NOT EXISTS subgrupo_id INT UNSIGNED DEFAULT NULL + COMMENT 'FK lab_empresa_subgrupos.id', + ADD COLUMN IF NOT EXISTS autorizacion VARCHAR(100) DEFAULT NULL + COMMENT 'Número de autorización EPS', + ADD COLUMN IF NOT EXISTS diag_ppal VARCHAR(20) DEFAULT NULL + COMMENT 'Diagnóstico principal CIE-10', + ADD COLUMN IF NOT EXISTS items_precio JSON DEFAULT NULL + COMMENT 'Snapshot de precios al momento de la recepción'; diff --git a/modules/turnero/api/create_solicitud.php b/modules/turnero/api/create_solicitud.php index 1226ab8..ede802f 100644 --- a/modules/turnero/api/create_solicitud.php +++ b/modules/turnero/api/create_solicitud.php @@ -32,7 +32,13 @@ $obs = isset($datos['observaciones']) ? trim((string) $datos['observac $numOrden = isset($datos['numero_orden']) ? trim((string) $datos['numero_orden']) : null; $embarazada = !empty($datos['embarazada']) ? 1 : 0; $soloMuestras = !empty($datos['solo_muestras']) ? 1 : 0; -$medicoId = !empty($datos['medico_id']) ? (int)$datos['medico_id'] : null; +$medicoId = !empty($datos['medico_id']) ? (int)$datos['medico_id'] : null; +$nitEmpresa = !empty($datos['nit_empresa']) ? trim((string)$datos['nit_empresa']) : null; +$subgrupoId = !empty($datos['subgrupo_id']) ? (int)$datos['subgrupo_id'] : null; +$autorizacion = !empty($datos['autorizacion']) ? trim((string)$datos['autorizacion']) : null; +$diagPpal = !empty($datos['diag_ppal']) ? trim((string)$datos['diag_ppal']) : null; +$itemsPrecio = isset($datos['items_precio']) && is_array($datos['items_precio']) + ? json_encode($datos['items_precio']) : null; $pagosDetalle = null; if ($metodoPago === 'combinado' && isset($datos['pagos_detalle']) && is_array($datos['pagos_detalle'])) { $pd = $datos['pagos_detalle']; @@ -117,12 +123,15 @@ try { $stmt = $pdo->prepare( "INSERT INTO turnero_solicitudes - (turno_id, paciente_id, lugar_id, numero_orden, total_cobrado, metodo_pago, pagos_detalle, observaciones, embarazada, solo_muestras, medico_id, creado_por) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + (turno_id, paciente_id, lugar_id, numero_orden, total_cobrado, metodo_pago, pagos_detalle, + observaciones, embarazada, solo_muestras, medico_id, + nit_empresa, subgrupo_id, autorizacion, diag_ppal, items_precio, creado_por) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" ); $stmt->execute([ $turnoId, $pacienteId, $lugarId, $numOrden, - $total, $metodoPago ?: null, $pagosDetalle, $obs ?: null, $embarazada, $soloMuestras, $medicoId, adminId(), + $total, $metodoPago ?: null, $pagosDetalle, $obs ?: null, $embarazada, $soloMuestras, $medicoId, + $nitEmpresa, $subgrupoId, $autorizacion, $diagPpal, $itemsPrecio, adminId(), ]); $solicitudId = (int) $pdo->lastInsertId(); diff --git a/modules/turnero/api/get_precios_examenes.php b/modules/turnero/api/get_precios_examenes.php new file mode 100644 index 0000000..2eaf4fb --- /dev/null +++ b/modules/turnero/api/get_precios_examenes.php @@ -0,0 +1,149 @@ + [], + 'subtotal' => 0, + 'descuento_pct' => 0, + 'descuento_val' => 0, + 'total' => 0, + 'tarifa_id' => null, + 'tarifa_nombre' => null, +]); + +$pdo = db(); + +// ── Resolver tarifa efectiva ─────────────────────────────────── +$tarifaId = null; +$tarifaNombre = null; +$descuentoPct = 0; + +if ($nitEmpresa !== '') { + $stmtE = $pdo->prepare( + "SELECT e.tarifa_id, e.descuento_pct, ti.nombre AS tarifa_nombre + FROM lab_empresas e + LEFT JOIN lab_tarifas_id ti ON ti.id = e.tarifa_id + WHERE e.nit = ?" + ); + $stmtE->execute([$nitEmpresa]); + $empresa = $stmtE->fetch(PDO::FETCH_ASSOC); + + if ($empresa) { + $tarifaId = $empresa['tarifa_id']; + $tarifaNombre = $empresa['tarifa_nombre']; + $descuentoPct = (float)$empresa['descuento_pct']; + + // Si el subgrupo tiene tarifa propia, override + if ($subgrupoId > 0) { + $stmtS = $pdo->prepare( + "SELECT es.tarifa_id, ti.nombre AS tarifa_nombre + FROM lab_empresa_subgrupos es + LEFT JOIN lab_tarifas_id ti ON ti.id = es.tarifa_id + WHERE es.id = ? AND es.nit_empresa = ? AND es.tarifa_id IS NOT NULL" + ); + $stmtS->execute([$subgrupoId, $nitEmpresa]); + $sub = $stmtS->fetch(PDO::FETCH_ASSOC); + if ($sub) { + $tarifaId = $sub['tarifa_id']; + $tarifaNombre = $sub['tarifa_nombre']; + } + } + } +} + +// ── Obtener nombre/codigo de los exámenes ───────────────────── +$ph = implode(',', array_fill(0, count($examIds), '?')); +$stmtX = $pdo->prepare( + "SELECT id AS exam_tipo_id, codigo, nombre FROM exam_tipos WHERE id IN ($ph)" +); +$stmtX->execute($examIds); +$examMap = []; +foreach ($stmtX->fetchAll(PDO::FETCH_ASSOC) as $r) { + $examMap[(int)$r['exam_tipo_id']] = $r; +} + +// ── Obtener precios desde lab_tarifas (con derivación por %) ── +$items = []; +$subtotal = 0; + +foreach ($examIds as $examId) { + $examen = $examMap[$examId] ?? ['codigo' => '?', 'nombre' => 'Desconocido']; + $valorFinal = null; + $valorBase = null; + $tienePrecio = false; + + if ($tarifaId !== null) { + // Intentar precio en la tarifa exacta + $stmtP = $pdo->prepare( + "SELECT lt.valor, ti.porcentaje, ti.tarifa_origen, + lt_base.valor AS valor_origen + FROM lab_tarifas lt + JOIN lab_tarifas_id ti ON ti.id = lt.tarifa_id + LEFT JOIN lab_tarifas lt_base ON lt_base.exam_tipo_id = lt.exam_tipo_id + AND lt_base.tarifa_id = ti.tarifa_origen + WHERE lt.exam_tipo_id = ? AND lt.tarifa_id = ?" + ); + $stmtP->execute([$examId, $tarifaId]); + $precio = $stmtP->fetch(PDO::FETCH_ASSOC); + + if ($precio) { + $tienePrecio = true; + $valorBase = (float)$precio['valor']; + if ((float)$precio['porcentaje'] > 0 && $precio['tarifa_origen'] && $precio['valor_origen'] !== null) { + $valorFinal = round((float)$precio['valor_origen'] * (1 + (float)$precio['porcentaje'] / 100), 0); + } else { + $valorFinal = $valorBase; + } + } + } + + // Si no hay precio, dejar null (sin tarifa o sin mapeo) + $subtotal += $valorFinal ?? 0; + + $items[] = [ + 'exam_tipo_id' => $examId, + 'codigo' => $examen['codigo'], + 'nombre' => $examen['nombre'], + 'valor_base' => $valorBase, + 'valor_final' => $valorFinal, + 'tiene_precio' => $tienePrecio, + ]; +} + +// ── Aplicar descuento empresa ───────────────────────────────── +$descuentoVal = round($subtotal * $descuentoPct / 100, 0); +$total = $subtotal - $descuentoVal; + +jsonOk([ + 'items' => $items, + 'subtotal' => $subtotal, + 'descuento_pct' => $descuentoPct, + 'descuento_val' => $descuentoVal, + 'total' => $total, + 'tarifa_id' => $tarifaId, + 'tarifa_nombre' => $tarifaNombre, +]); diff --git a/modules/turnero/views/recepcion.php b/modules/turnero/views/recepcion.php index 0022a9f..aea1cd3 100644 --- a/modules/turnero/views/recepcion.php +++ b/modules/turnero/views/recepcion.php @@ -608,6 +608,80 @@ document.addEventListener('DOMContentLoaded', function() { + +
+
Empresa / Convenio (opcional)
+ +
+
+
+
+
+
+ +
+
+
+ +
+
    +
    +
    + + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    +
    + + Precios + + +
    +
    +
    + Total calculado + $0 +
    +
    + Descuento empresa (%) + +
    + +
    +
    +
    Cobro (opcional)
    @@ -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 = '
  • Sin resultados
  • '; ul.classList.remove('d-none'); return; } + ul.innerHTML = list.map(e => ` +
  • +
    ${escHtml(e.nombre)}
    +
    NIT ${escHtml(e.nit)}${e.tarifa_nombre ? ' · ' + escHtml(e.tarifa_nombre) : ''}
    +
  • + `).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 = '' + + subs.map(s => ``).join(''); + wrapSub.classList.remove('d-none'); + } else { + selSub.innerHTML = ''; + 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 = ''; + 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 = '
    Sin precios configurados para esta tarifa
    '; + } else { + tabla.innerHTML = j.items.map(i => ` +
    + ${escHtml(i.nombre)} + + ${i.tiene_precio ? '$' + fmt(i.valor_final) : 'Sin precio'} + +
    + `).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 = '/api/lab/'; + function escHtml(str) { const d = document.createElement('div'); d.appendChild(document.createTextNode(String(str)));