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 = '= BASE_URL ?>/api/lab/';
+
function escHtml(str) {
const d = document.createElement('div');
d.appendChild(document.createTextNode(String(str)));