From b9bb4fc6280c92ac9eed5755a82f6745aa9b9939 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Sat, 4 Jul 2026 22:05:14 -0500 Subject: [PATCH] =?UTF-8?q?feat(lab=5Fempresas):=20m=C3=B3dulo=20CRUD=20de?= =?UTF-8?q?=20empresas,=20convenios=20y=20tarifas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nuevo módulo lab_empresas: lista paginada, modal create/edit empresa, gestión de subgrupos inline y CRUD de catálogo lab_tarifas_id - APIs: empresas.php (list/get/save/toggle/delete), empresa_subgrupos.php, tarifas_id.php - Migración 09 registra el módulo en system_modules y asigna permiso admin Co-Authored-By: Claude Sonnet 4.6 --- api/lab/empresa_subgrupos.php | 67 +++ api/lab/empresas.php | 162 +++++ api/lab/tarifas_id.php | 71 +++ .../20260704_lis_09_module_empresas.sql | 17 + modules/lab_empresas/module.php | 16 + modules/lab_empresas/views/index.php | 556 ++++++++++++++++++ modules/lab_empresas/views/tarifas.php | 212 +++++++ 7 files changed, 1101 insertions(+) create mode 100644 api/lab/empresa_subgrupos.php create mode 100644 api/lab/empresas.php create mode 100644 api/lab/tarifas_id.php create mode 100644 migrations/20260704_lis_09_module_empresas.sql create mode 100644 modules/lab_empresas/module.php create mode 100644 modules/lab_empresas/views/index.php create mode 100644 modules/lab_empresas/views/tarifas.php diff --git a/api/lab/empresa_subgrupos.php b/api/lab/empresa_subgrupos.php new file mode 100644 index 0000000..bb7729f --- /dev/null +++ b/api/lab/empresa_subgrupos.php @@ -0,0 +1,67 @@ +prepare( + "SELECT es.*, ti.nombre AS tarifa_nombre + FROM lab_empresa_subgrupos es + LEFT JOIN lab_tarifas_id ti ON ti.id = es.tarifa_id + WHERE es.nit_empresa = ? + ORDER BY es.subgrupo ASC" + ); + $s->execute([$nit]); + jsonOk(['subgrupos' => $s->fetchAll(PDO::FETCH_ASSOC)]); +} + +requireMethod('POST'); +$data = json_decode(file_get_contents('php://input'), true) ?? []; +$action = $data['action'] ?? ''; +$pdo = db(); + +if ($action === 'delete') { + $id = (int)($data['id'] ?? 0); + if ($id <= 0) jsonError('id requerido'); + $pdo->prepare("DELETE FROM lab_empresa_subgrupos WHERE id = ?")->execute([$id]); + jsonOk(['deleted' => true]); +} + +if ($action === 'save') { + $id = (int)($data['id'] ?? 0); + $nitEmpresa = trim($data['nit_empresa'] ?? ''); + $subgrupo = trim($data['subgrupo'] ?? ''); + if ($nitEmpresa === '') jsonError('nit_empresa requerido'); + if ($subgrupo === '') jsonError('subgrupo requerido'); + + $tarifaId = isset($data['tarifa_id']) && $data['tarifa_id'] !== '' ? (int)$data['tarifa_id'] : null; + $refSub = trim($data['ref_subgrupo'] ?? '') ?: null; + $codCon = trim($data['cod_contrato'] ?? '') ?: null; + + if ($id > 0) { + $pdo->prepare( + "UPDATE lab_empresa_subgrupos + SET subgrupo=?, tarifa_id=?, ref_subgrupo=?, cod_contrato=? + WHERE id=? AND nit_empresa=?" + )->execute([$subgrupo, $tarifaId, $refSub, $codCon, $id, $nitEmpresa]); + } else { + $pdo->prepare( + "INSERT INTO lab_empresa_subgrupos (nit_empresa, subgrupo, tarifa_id, ref_subgrupo, cod_contrato) + VALUES (?, ?, ?, ?, ?)" + )->execute([$nitEmpresa, $subgrupo, $tarifaId, $refSub, $codCon]); + $id = (int)$pdo->lastInsertId(); + } + jsonOk(['id' => $id]); +} + +jsonError('Acción no reconocida', 400); diff --git a/api/lab/empresas.php b/api/lab/empresas.php new file mode 100644 index 0000000..e364f54 --- /dev/null +++ b/api/lab/empresas.php @@ -0,0 +1,162 @@ +prepare( + "SELECT e.*, ti.nombre AS tarifa_nombre + FROM lab_empresas e + LEFT JOIN lab_tarifas_id ti ON ti.id = e.tarifa_id + WHERE e.nit = ?" + ); + $e->execute([$nit]); + $empresa = $e->fetch(PDO::FETCH_ASSOC); + if (!$empresa) jsonError('Empresa no encontrada', 404); + + $s = $pdo->prepare( + "SELECT es.*, ti.nombre AS tarifa_nombre + FROM lab_empresa_subgrupos es + LEFT JOIN lab_tarifas_id ti ON ti.id = es.tarifa_id + WHERE es.nit_empresa = ? + ORDER BY es.subgrupo ASC" + ); + $s->execute([$nit]); + $empresa['subgrupos'] = $s->fetchAll(PDO::FETCH_ASSOC); + + jsonOk(['empresa' => $empresa]); + } + + // list + $search = trim($_GET['search'] ?? ''); + $activa = isset($_GET['activa']) && $_GET['activa'] !== '' ? (int)$_GET['activa'] : null; + $page = max(1, (int)($_GET['page'] ?? 1)); + $limit = max(1, min(100, (int)($_GET['limit'] ?? 30))); + $offset = ($page - 1) * $limit; + + $where = []; + $params = []; + if ($search !== '') { + $where[] = '(e.nombre LIKE ? OR e.nit LIKE ? OR e.razon_social LIKE ?)'; + $like = "%{$search}%"; + $params[] = $like; $params[] = $like; $params[] = $like; + } + if ($activa !== null) { + $where[] = 'e.activa = ?'; + $params[] = $activa; + } + + $wSql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; + + $total = $pdo->prepare("SELECT COUNT(*) FROM lab_empresas e $wSql"); + $total->execute($params); + $totalRows = (int)$total->fetchColumn(); + + $rows = $pdo->prepare( + "SELECT e.nit, e.nombre, e.razon_social, e.tarifa_id, ti.nombre AS tarifa_nombre, + e.descuento_pct, e.codigo_eps, e.tipo_usuario, e.req_autoriza, e.activa, + (SELECT COUNT(*) FROM lab_empresa_subgrupos es WHERE es.nit_empresa = e.nit) AS total_subgrupos + FROM lab_empresas e + LEFT JOIN lab_tarifas_id ti ON ti.id = e.tarifa_id + $wSql + ORDER BY e.nombre ASC + LIMIT $limit OFFSET $offset" + ); + $rows->execute($params); + + jsonOk([ + 'empresas' => $rows->fetchAll(PDO::FETCH_ASSOC), + 'total' => $totalRows, + 'page' => $page, + 'limit' => $limit, + 'pages' => (int)ceil($totalRows / $limit), + ]); +} + +// ─── POST ──────────────────────────────────────────────────────────────────── +requireMethod('POST'); +$data = json_decode(file_get_contents('php://input'), true) ?? []; +$action = $data['action'] ?? ''; +$pdo = db(); + +if ($action === 'toggle') { + $nit = trim($data['nit'] ?? ''); + if ($nit === '') jsonError('nit requerido'); + $pdo->prepare("UPDATE lab_empresas SET activa = 1 - activa WHERE nit = ?")->execute([$nit]); + $activa = (int)$pdo->prepare("SELECT activa FROM lab_empresas WHERE nit=?")->execute([$nit]); + $row = $pdo->prepare("SELECT activa FROM lab_empresas WHERE nit=?"); + $row->execute([$nit]); + jsonOk(['activa' => (bool)(int)$row->fetchColumn()]); +} + +if ($action === 'delete') { + $nit = trim($data['nit'] ?? ''); + if ($nit === '') jsonError('nit requerido'); + // Block deletion if empresa has recepciones (historical) + $chk = $pdo->prepare("SELECT COUNT(*) FROM lab_recepciones WHERE nit_empresa = ? LIMIT 1"); + $chk->execute([$nit]); + if ((int)$chk->fetchColumn() > 0) { + jsonError('No se puede eliminar: la empresa tiene recepciones históricas registradas.', 409); + } + $pdo->prepare("DELETE FROM lab_empresas WHERE nit = ?")->execute([$nit]); + jsonOk(['deleted' => true]); +} + +if ($action === 'save') { + $nit = trim($data['nit'] ?? ''); + $nombre = trim($data['nombre'] ?? ''); + if ($nit === '') jsonError('El NIT es obligatorio'); + if ($nombre === '') jsonError('El nombre es obligatorio'); + + $fields = [ + 'nombre' => $nombre, + 'razon_social' => trim($data['razon_social'] ?? '') ?: null, + 'tarifa_id' => isset($data['tarifa_id']) && $data['tarifa_id'] !== '' ? (int)$data['tarifa_id'] : null, + 'descuento_pct' => isset($data['descuento_pct']) ? (float)$data['descuento_pct'] : 0, + 'codigo_eps' => trim($data['codigo_eps'] ?? '') ?: null, + 'tipo_usuario' => trim($data['tipo_usuario'] ?? '') ?: null, + 'tipo_usuario_sispro'=> trim($data['tipo_usuario_sispro']?? '') ?: null, + 'cod_contrato' => trim($data['cod_contrato'] ?? '') ?: null, + 'cod_tercero' => trim($data['cod_tercero'] ?? '') ?: null, + 'centro_costo' => trim($data['centro_costo'] ?? '') ?: null, + 'req_autoriza' => isset($data['req_autoriza']) ? (int)(bool)$data['req_autoriza'] : 0, + 'activa' => isset($data['activa']) ? (int)(bool)$data['activa'] : 1, + ]; + + // Check if exists + $exists = $pdo->prepare("SELECT nit FROM lab_empresas WHERE nit = ?"); + $exists->execute([$nit]); + $isNew = !$exists->fetch(); + + if ($isNew) { + $cols = implode(', ', array_map(fn($k) => "`$k`", array_keys($fields))); + $ph = implode(', ', array_fill(0, count($fields), '?')); + $pdo->prepare("INSERT INTO lab_empresas (nit, $cols) VALUES (?, $ph)") + ->execute(array_merge([$nit], array_values($fields))); + } else { + $sets = implode(', ', array_map(fn($k) => "`$k` = ?", array_keys($fields))); + $pdo->prepare("UPDATE lab_empresas SET $sets WHERE nit = ?") + ->execute(array_merge(array_values($fields), [$nit])); + } + + jsonOk(['nit' => $nit, 'created' => $isNew]); +} + +jsonError('Acción no reconocida', 400); diff --git a/api/lab/tarifas_id.php b/api/lab/tarifas_id.php new file mode 100644 index 0000000..c5ec36f --- /dev/null +++ b/api/lab/tarifas_id.php @@ -0,0 +1,71 @@ +query( + "SELECT t.id, t.nombre, t.porcentaje, t.tarifa_origen, + tb.nombre AS tarifa_origen_nombre + FROM lab_tarifas_id t + LEFT JOIN lab_tarifas_id tb ON tb.id = t.tarifa_origen + ORDER BY t.id ASC" + )->fetchAll(PDO::FETCH_ASSOC); + jsonOk(['tarifas' => $rows]); +} + +requireMethod('POST'); +$data = json_decode(file_get_contents('php://input'), true) ?? []; +$action = $data['action'] ?? ''; + +if ($action === 'delete') { + $id = (int)($data['id'] ?? 0); + if ($id <= 0) jsonError('id requerido'); + $chk = $pdo->prepare("SELECT COUNT(*) FROM lab_tarifas WHERE tarifa_id = ? LIMIT 1"); + $chk->execute([$id]); + if ((int)$chk->fetchColumn() > 0) { + jsonError('No se puede eliminar: la tarifa tiene precios de exámenes asociados.', 409); + } + $pdo->prepare("DELETE FROM lab_tarifas_id WHERE id = ?")->execute([$id]); + jsonOk(['deleted' => true]); +} + +if ($action === 'save') { + $id = isset($data['id']) && $data['id'] !== '' ? (int)$data['id'] : null; + $nombre = trim($data['nombre'] ?? ''); + $porcentaje = isset($data['porcentaje']) ? (float)$data['porcentaje'] : 0; + $origen = isset($data['tarifa_origen']) && $data['tarifa_origen'] !== '' ? (int)$data['tarifa_origen'] : null; + + if ($nombre === '') jsonError('nombre requerido'); + + if ($id !== null) { + $chk = $pdo->prepare("SELECT id FROM lab_tarifas_id WHERE id = ?"); + $chk->execute([$id]); + if ($chk->fetch()) { + $pdo->prepare( + "UPDATE lab_tarifas_id SET nombre=?, porcentaje=?, tarifa_origen=? WHERE id=?" + )->execute([$nombre, $porcentaje, $origen, $id]); + } else { + $pdo->prepare( + "INSERT INTO lab_tarifas_id (id, nombre, porcentaje, tarifa_origen) VALUES (?,?,?,?)" + )->execute([$id, $nombre, $porcentaje, $origen]); + } + } else { + $maxId = (int)$pdo->query("SELECT COALESCE(MAX(id),0)+1 FROM lab_tarifas_id")->fetchColumn(); + $pdo->prepare( + "INSERT INTO lab_tarifas_id (id, nombre, porcentaje, tarifa_origen) VALUES (?,?,?,?)" + )->execute([$maxId, $nombre, $porcentaje, $origen]); + $id = $maxId; + } + jsonOk(['id' => $id]); +} + +jsonError('Acción no reconocida', 400); diff --git a/migrations/20260704_lis_09_module_empresas.sql b/migrations/20260704_lis_09_module_empresas.sql new file mode 100644 index 0000000..3c5411f --- /dev/null +++ b/migrations/20260704_lis_09_module_empresas.sql @@ -0,0 +1,17 @@ +-- ============================================================= +-- LIS 09 — Registra módulo lab_empresas en system_modules +-- y agrega permiso por defecto para el rol admin. +-- Seguro para re-ejecutar (INSERT IGNORE). +-- ============================================================= + +INSERT IGNORE INTO system_modules + (slug, name, icon, category, route, is_active, sort_order, oleada, description) +VALUES + ('lab_empresas', 'Empresas y Convenios', 'fas fa-building', 'clinico', + '/erp.php?m=lab_empresas&v=index', 1, 65, 2, + 'Gestión de empresas, EPS, convenios, subgrupos y catálogo de tarifas'); + +-- Permiso automático para rol admin (role_id = 1) +INSERT IGNORE INTO role_modules (role_id, module_slug) +SELECT 1, 'lab_empresas' +WHERE EXISTS (SELECT 1 FROM roles WHERE id = 1); diff --git a/modules/lab_empresas/module.php b/modules/lab_empresas/module.php new file mode 100644 index 0000000..97b65ab --- /dev/null +++ b/modules/lab_empresas/module.php @@ -0,0 +1,16 @@ + 'lab_empresas', + 'name' => 'Empresas y Convenios', + 'icon' => 'fas fa-building', + 'category' => 'clinico', + 'route' => '/erp.php?m=lab_empresas&v=index', + 'is_active' => true, + 'sort_order' => 65, + 'oleada' => 2, + 'description' => 'Gestión de empresas, EPS, convenios, subgrupos y tarifas.', + 'links' => [ + ['name' => 'Empresas', 'icon' => 'fas fa-building', 'route' => '/erp.php?m=lab_empresas&v=index'], + ['name' => 'Tarifas', 'icon' => 'fas fa-tags', 'route' => '/erp.php?m=lab_empresas&v=tarifas'], + ], +]; diff --git a/modules/lab_empresas/views/index.php b/modules/lab_empresas/views/index.php new file mode 100644 index 0000000..cd648da --- /dev/null +++ b/modules/lab_empresas/views/index.php @@ -0,0 +1,556 @@ + +
+ + +
+
+

Empresas y Convenios

+ EPS, IPS, convenios y particulares con tarifa especial +
+
+ + Tarifas + + +
+
+ + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + +
NITNombreTarifaDcto%SubgruposAutorizaEstado
Cargando…
+
+
+ + +
+
+ + + + + + + + + + + diff --git a/modules/lab_empresas/views/tarifas.php b/modules/lab_empresas/views/tarifas.php new file mode 100644 index 0000000..8e57bfe --- /dev/null +++ b/modules/lab_empresas/views/tarifas.php @@ -0,0 +1,212 @@ + +
+ +
+
+

Catálogo de Tarifas

+ IDs de tarifa usados en el motor de precios +
+
+ + Empresas + + +
+
+ +
+ +
+
+
+ + + + + + + + + + + + + +
IDNombre%Origen
Cargando…
+
+
+

+ + Tarifas con % > 0 y Origen se calculan automáticamente + como: precio_origen × (1 + %/100). +

+
+ + +
+
+
+
Nueva tarifa
+
+
+
+ + +
El ID debe coincidir con el ID Firebird si se va a migrar.
+
+
+ + +
+
+ +
+ + % +
+
0 = precios fijos. >0 = derivada de otra tarifa.
+
+
+ + +
Solo si esta tarifa deriva de otra por porcentaje.
+
+
+
+ + +
+
+
+
+
+
+ + +