feat(lab_empresas): módulo CRUD de empresas, convenios y tarifas
- 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
5fedd233f4
commit
b9bb4fc628
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* /api/lab/empresa_subgrupos.php
|
||||
*
|
||||
* GET ?nit_empresa= → subgrupos de la empresa
|
||||
* POST {action:save, ...} → upsert subgrupo
|
||||
* POST {action:delete, id} → elimina subgrupo
|
||||
*/
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
if ($method === 'GET') {
|
||||
$nit = trim($_GET['nit_empresa'] ?? '');
|
||||
if ($nit === '') jsonError('nit_empresa requerido');
|
||||
$pdo = db();
|
||||
$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]);
|
||||
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);
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* /api/lab/empresas.php
|
||||
*
|
||||
* GET ?action=list [search, activa, page, limit] → lista paginada
|
||||
* GET ?action=get &nit= → empresa + subgrupos
|
||||
* POST {action:save, nit, nombre, ...} → upsert empresa
|
||||
* POST {action:toggle, nit} → activa/inactiva
|
||||
* POST {action:delete, nit} → elimina (si sin recepciones)
|
||||
*/
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// ─── GET ─────────────────────────────────────────────────────────────────────
|
||||
if ($method === 'GET') {
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$pdo = db();
|
||||
|
||||
if ($action === 'get') {
|
||||
$nit = trim($_GET['nit'] ?? '');
|
||||
if ($nit === '') jsonError('nit requerido');
|
||||
|
||||
$e = $pdo->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);
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* /api/lab/tarifas_id.php
|
||||
*
|
||||
* GET → lista todas las tarifas (para selects en formularios)
|
||||
* POST {action:save, id?, nombre, porcentaje, tarifa_origen?} → upsert
|
||||
* POST {action:delete, id} → elimina si sin precios asociados
|
||||
*/
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$pdo = db();
|
||||
|
||||
if ($method === 'GET') {
|
||||
$rows = $pdo->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);
|
||||
@@ -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);
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
return [
|
||||
'slug' => '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'],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,556 @@
|
||||
<?php
|
||||
/**
|
||||
* modules/lab_empresas/views/index.php
|
||||
* CRUD de empresas/convenios y sus subgrupos.
|
||||
*/
|
||||
require_once APP_ROOT . '/config/config.php';
|
||||
Layout::open('Empresas y Convenios', 'fas fa-building');
|
||||
?>
|
||||
<div class="container-fluid py-3">
|
||||
|
||||
<!-- Encabezado -->
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||
<div>
|
||||
<h4 class="mb-0"><i class="fas fa-building me-2 text-primary"></i>Empresas y Convenios</h4>
|
||||
<small class="text-muted">EPS, IPS, convenios y particulares con tarifa especial</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="<?= BASE_URL ?>/erp.php?m=lab_empresas&v=tarifas"
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-tags me-1"></i>Tarifas
|
||||
</a>
|
||||
<button class="btn btn-primary btn-sm" onclick="abrirModal()">
|
||||
<i class="fas fa-plus me-1"></i>Nueva empresa
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body py-2">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-sm-5 col-md-4">
|
||||
<input type="search" id="filtroSearch" class="form-control form-control-sm"
|
||||
placeholder="Buscar por nombre, NIT…" oninput="debounceCargar()">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<select id="filtroActiva" class="form-select form-select-sm" onchange="cargarEmpresas()">
|
||||
<option value="">Todas</option>
|
||||
<option value="1" selected>Activas</option>
|
||||
<option value="0">Inactivas</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto ms-auto">
|
||||
<span class="text-muted small" id="lblTotal"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm mb-0" id="tablaEmpresas">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>NIT</th>
|
||||
<th>Nombre</th>
|
||||
<th>Tarifa</th>
|
||||
<th class="text-end">Dcto%</th>
|
||||
<th class="text-center">Subgrupos</th>
|
||||
<th class="text-center">Autoriza</th>
|
||||
<th class="text-center">Estado</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbodyEmpresas">
|
||||
<tr><td colspan="8" class="text-center py-4 text-muted">Cargando…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Paginación -->
|
||||
<div class="card-footer d-flex justify-content-between align-items-center py-1">
|
||||
<button class="btn btn-outline-secondary btn-sm" id="btnPrev" onclick="cambiarPagina(-1)">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<span class="small text-muted" id="lblPagina"></span>
|
||||
<button class="btn btn-outline-secondary btn-sm" id="btnNext" onclick="cambiarPagina(1)">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
Modal empresa (crear / editar)
|
||||
═══════════════════════════════════════════════════════════ -->
|
||||
<div class="modal fade" id="modalEmpresa" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalEmpresaTitulo">Nueva empresa</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="formEmpresa" novalidate>
|
||||
<input type="hidden" id="fNitOrig" value="">
|
||||
|
||||
<!-- Fila 1: NIT + Nombre -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label fw-semibold small">NIT <span class="text-danger">*</span></label>
|
||||
<input type="text" id="fNit" class="form-control form-control-sm"
|
||||
placeholder="900123456-7" maxlength="20" required>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<label class="form-label fw-semibold small">Nombre <span class="text-danger">*</span></label>
|
||||
<input type="text" id="fNombre" class="form-control form-control-sm"
|
||||
placeholder="Nombre de la empresa" maxlength="200" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fila 2: Razón social -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold small">Razón social</label>
|
||||
<input type="text" id="fRazonSocial" class="form-control form-control-sm"
|
||||
placeholder="Razón social completa" maxlength="200">
|
||||
</div>
|
||||
|
||||
<!-- Fila 3: Tarifa + Descuento -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label fw-semibold small">Tarifa</label>
|
||||
<select id="fTarifaId" class="form-select form-select-sm">
|
||||
<option value="">— Sin tarifa especial —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label class="form-label fw-semibold small">Descuento %</label>
|
||||
<input type="number" id="fDescuentoPct" class="form-control form-control-sm"
|
||||
value="0" min="0" max="100" step="0.01">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label class="form-label fw-semibold small">Cód. EPS</label>
|
||||
<input type="text" id="fCodigoEps" class="form-control form-control-sm"
|
||||
placeholder="EPS001" maxlength="20">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fila 4: Tipo usuario -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label fw-semibold small">Tipo usuario</label>
|
||||
<select id="fTipoUsuario" class="form-select form-select-sm">
|
||||
<option value="">—</option>
|
||||
<option value="01">01 - Contributivo</option>
|
||||
<option value="02">02 - Subsidiado</option>
|
||||
<option value="03">03 - Vinculado</option>
|
||||
<option value="04">04 - Particular</option>
|
||||
<option value="05">05 - ARP</option>
|
||||
<option value="06">06 - Póliza</option>
|
||||
<option value="07">07 - Estudiante</option>
|
||||
<option value="08">08 - Empleado</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label fw-semibold small">Tipo usuario SISPRO</label>
|
||||
<input type="text" id="fTipoUsuarioSispro" class="form-control form-control-sm"
|
||||
placeholder="Código SISPRO" maxlength="10">
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label fw-semibold small">Cod. tercero</label>
|
||||
<input type="text" id="fCodTercero" class="form-control form-control-sm"
|
||||
placeholder="Cód. tercero" maxlength="50">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fila 5: Contrato / Centro costo -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label fw-semibold small">Cód. contrato</label>
|
||||
<input type="text" id="fCodContrato" class="form-control form-control-sm"
|
||||
placeholder="Número de contrato" maxlength="50">
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label fw-semibold small">Centro de costo</label>
|
||||
<input type="text" id="fCentroCosto" class="form-control form-control-sm"
|
||||
placeholder="Centro de costo" maxlength="50">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fila 6: Checks -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-auto">
|
||||
<div class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="fReqAutoriza">
|
||||
<label class="form-check-label small" for="fReqAutoriza">
|
||||
Exige número de autorización
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="fActiva" checked>
|
||||
<label class="form-check-label small" for="fActiva">Activa</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mensaje de error -->
|
||||
<div id="formError" class="alert alert-danger d-none py-2 small"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="btnGuardarEmpresa" onclick="guardarEmpresa()">
|
||||
<i class="fas fa-save me-1"></i>Guardar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
Modal subgrupos
|
||||
═══════════════════════════════════════════════════════════ -->
|
||||
<div class="modal fade" id="modalSubgrupos" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-layer-group me-2 text-secondary"></i>
|
||||
Subgrupos — <span id="subNitNombre"></span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="listaSubgrupos" class="mb-3"></div>
|
||||
<hr>
|
||||
<h6 class="small fw-semibold text-muted text-uppercase mb-2">Agregar / editar subgrupo</h6>
|
||||
<input type="hidden" id="subId" value="">
|
||||
<input type="hidden" id="subNitEmpresa" value="">
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-12">
|
||||
<input type="text" id="subNombre" class="form-control form-control-sm"
|
||||
placeholder="Nombre del subgrupo" maxlength="100">
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<select id="subTarifaId" class="form-select form-select-sm">
|
||||
<option value="">— Tarifa de la empresa —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" id="subRefSubgrupo" class="form-control form-control-sm"
|
||||
placeholder="Ref. subgrupo" maxlength="50">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input type="text" id="subCodContrato" class="form-control form-control-sm"
|
||||
placeholder="Cód. contrato subgrupo" maxlength="50">
|
||||
</div>
|
||||
</div>
|
||||
<div id="subError" class="alert alert-danger d-none py-2 small mb-2"></div>
|
||||
<button class="btn btn-sm btn-primary w-100" onclick="guardarSubgrupo()">
|
||||
<i class="fas fa-save me-1"></i>Guardar subgrupo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.badge-tarifa { font-size:.72rem; background:#e0f2fe; color:#0369a1; border-radius:4px; padding:2px 7px; }
|
||||
.badge-activa { font-size:.72rem; }
|
||||
.sub-row { background:#f8fafc; border-radius:6px; padding:8px 12px; margin-bottom:6px;
|
||||
border:1px solid #e2e8f0; display:flex; align-items:center; gap:8px; }
|
||||
.sub-row .sub-info { flex:1 }
|
||||
.sub-row .sub-info .sub-nombre { font-weight:600; font-size:.9rem; }
|
||||
.sub-row .sub-info .sub-meta { font-size:.78rem; color:#64748b; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const BASE = '<?= BASE_URL ?>';
|
||||
let _pagina = 1;
|
||||
let _pages = 1;
|
||||
let _tarifas = [];
|
||||
let _modalEmpresa, _modalSubgrupos;
|
||||
|
||||
// ─── Init ─────────────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
_modalEmpresa = new bootstrap.Modal(document.getElementById('modalEmpresa'));
|
||||
_modalSubgrupos = new bootstrap.Modal(document.getElementById('modalSubgrupos'));
|
||||
cargarTarifas().then(() => cargarEmpresas());
|
||||
});
|
||||
|
||||
// ─── Tarifas ──────────────────────────────────────────────────
|
||||
async function cargarTarifas() {
|
||||
const res = await fetch(`${BASE}/api/lab/tarifas_id.php`);
|
||||
const j = await res.json();
|
||||
_tarifas = j.tarifas || [];
|
||||
const opts = _tarifas.map(t =>
|
||||
`<option value="${t.id}">${escHtml(t.nombre)}${t.porcentaje > 0 ? ` (+${t.porcentaje}%)` : ''}</option>`
|
||||
).join('');
|
||||
document.getElementById('fTarifaId').innerHTML = '<option value="">— Sin tarifa especial —</option>' + opts;
|
||||
document.getElementById('subTarifaId').innerHTML = '<option value="">— Tarifa de la empresa —</option>' + opts;
|
||||
}
|
||||
|
||||
// ─── Lista empresas ───────────────────────────────────────────
|
||||
let _debTimer;
|
||||
function debounceCargar() {
|
||||
clearTimeout(_debTimer);
|
||||
_debTimer = setTimeout(() => { _pagina = 1; cargarEmpresas(); }, 350);
|
||||
}
|
||||
|
||||
async function cargarEmpresas() {
|
||||
const search = document.getElementById('filtroSearch').value.trim();
|
||||
const activa = document.getElementById('filtroActiva').value;
|
||||
const params = new URLSearchParams({ action:'list', page:_pagina, limit:30 });
|
||||
if (search) params.append('search', search);
|
||||
if (activa !== '') params.append('activa', activa);
|
||||
|
||||
const tbody = document.getElementById('tbodyEmpresas');
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-3 text-muted"><i class="fas fa-spinner fa-spin me-2"></i>Cargando…</td></tr>';
|
||||
|
||||
const res = await fetch(`${BASE}/api/lab/empresas.php?${params}`);
|
||||
const j = await res.json();
|
||||
|
||||
_pages = j.pages || 1;
|
||||
document.getElementById('lblTotal').textContent = `${j.total} empresa(s)`;
|
||||
document.getElementById('lblPagina').textContent = `Página ${_pagina} de ${_pages}`;
|
||||
document.getElementById('btnPrev').disabled = _pagina <= 1;
|
||||
document.getElementById('btnNext').disabled = _pagina >= _pages;
|
||||
|
||||
if (!j.empresas || !j.empresas.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-4 text-muted">Sin resultados</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = j.empresas.map(e => `
|
||||
<tr>
|
||||
<td class="text-monospace small fw-semibold">${escHtml(e.nit)}</td>
|
||||
<td>
|
||||
<div class="fw-semibold">${escHtml(e.nombre)}</div>
|
||||
${e.razon_social ? `<div class="text-muted small">${escHtml(e.razon_social)}</div>` : ''}
|
||||
</td>
|
||||
<td>${e.tarifa_nombre ? `<span class="badge-tarifa">${escHtml(e.tarifa_nombre)}</span>` : '<span class="text-muted small">—</span>'}</td>
|
||||
<td class="text-end small">${parseFloat(e.descuento_pct) > 0 ? escHtml(e.descuento_pct)+'%' : '—'}</td>
|
||||
<td class="text-center">
|
||||
${parseInt(e.total_subgrupos) > 0
|
||||
? `<button class="btn btn-link btn-sm p-0 text-primary" onclick="abrirSubgrupos('${escAttr(e.nit)}','${escAttr(e.nombre)}')">${e.total_subgrupos} <i class="fas fa-layer-group ms-1"></i></button>`
|
||||
: `<button class="btn btn-link btn-sm p-0 text-muted" onclick="abrirSubgrupos('${escAttr(e.nit)}','${escAttr(e.nombre)}')">+ subgrupo</button>`
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">${parseInt(e.req_autoriza) ? '<i class="fas fa-check-circle text-warning" title="Exige autorización"></i>' : '—'}</td>
|
||||
<td class="text-center">
|
||||
<span class="badge ${parseInt(e.activa) ? 'bg-success' : 'bg-secondary'} badge-activa">
|
||||
${parseInt(e.activa) ? 'Activa' : 'Inactiva'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end pe-2">
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-2 me-1" onclick="abrirModal('${escAttr(e.nit)}')" title="Editar">
|
||||
<i class="fas fa-pen"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-${parseInt(e.activa) ? 'warning' : 'success'} btn-sm py-0 px-2" onclick="toggleActiva('${escAttr(e.nit)}')" title="${parseInt(e.activa) ? 'Desactivar' : 'Activar'}">
|
||||
<i class="fas fa-${parseInt(e.activa) ? 'ban' : 'check'}"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function cambiarPagina(delta) {
|
||||
_pagina = Math.max(1, Math.min(_pages, _pagina + delta));
|
||||
cargarEmpresas();
|
||||
}
|
||||
|
||||
// ─── Modal empresa ────────────────────────────────────────────
|
||||
async function abrirModal(nit = null) {
|
||||
resetFormError();
|
||||
document.getElementById('fNitOrig').value = nit || '';
|
||||
document.getElementById('modalEmpresaTitulo').textContent = nit ? 'Editar empresa' : 'Nueva empresa';
|
||||
document.getElementById('fNit').readOnly = !!nit;
|
||||
|
||||
// Reset
|
||||
['fNit','fNombre','fRazonSocial','fCodigoEps','fTipoUsuarioSispro',
|
||||
'fCodTercero','fCodContrato','fCentroCosto'].forEach(id => document.getElementById(id).value = '');
|
||||
document.getElementById('fTarifaId').value = '';
|
||||
document.getElementById('fTipoUsuario').value = '';
|
||||
document.getElementById('fDescuentoPct').value= '0';
|
||||
document.getElementById('fReqAutoriza').checked = false;
|
||||
document.getElementById('fActiva').checked = true;
|
||||
|
||||
if (nit) {
|
||||
const res = await fetch(`${BASE}/api/lab/empresas.php?action=get&nit=${encodeURIComponent(nit)}`);
|
||||
const j = await res.json();
|
||||
const e = j.empresa;
|
||||
document.getElementById('fNit').value = e.nit;
|
||||
document.getElementById('fNombre').value = e.nombre;
|
||||
document.getElementById('fRazonSocial').value = e.razon_social || '';
|
||||
document.getElementById('fTarifaId').value = e.tarifa_id || '';
|
||||
document.getElementById('fDescuentoPct').value = e.descuento_pct || 0;
|
||||
document.getElementById('fCodigoEps').value = e.codigo_eps || '';
|
||||
document.getElementById('fTipoUsuario').value = e.tipo_usuario || '';
|
||||
document.getElementById('fTipoUsuarioSispro').value = e.tipo_usuario_sispro || '';
|
||||
document.getElementById('fCodTercero').value = e.cod_tercero || '';
|
||||
document.getElementById('fCodContrato').value = e.cod_contrato || '';
|
||||
document.getElementById('fCentroCosto').value = e.centro_costo || '';
|
||||
document.getElementById('fReqAutoriza').checked = !!parseInt(e.req_autoriza);
|
||||
document.getElementById('fActiva').checked = !!parseInt(e.activa);
|
||||
}
|
||||
_modalEmpresa.show();
|
||||
}
|
||||
|
||||
async function guardarEmpresa() {
|
||||
resetFormError();
|
||||
const nit = document.getElementById('fNit').value.trim();
|
||||
if (!nit) return showFormError('El NIT es obligatorio');
|
||||
if (!document.getElementById('fNombre').value.trim()) return showFormError('El nombre es obligatorio');
|
||||
|
||||
const btn = document.getElementById('btnGuardarEmpresa');
|
||||
btn.disabled = true;
|
||||
|
||||
const payload = {
|
||||
action: 'save',
|
||||
nit,
|
||||
nombre: document.getElementById('fNombre').value.trim(),
|
||||
razon_social: document.getElementById('fRazonSocial').value.trim(),
|
||||
tarifa_id: document.getElementById('fTarifaId').value,
|
||||
descuento_pct: document.getElementById('fDescuentoPct').value,
|
||||
codigo_eps: document.getElementById('fCodigoEps').value.trim(),
|
||||
tipo_usuario: document.getElementById('fTipoUsuario').value,
|
||||
tipo_usuario_sispro: document.getElementById('fTipoUsuarioSispro').value.trim(),
|
||||
cod_tercero: document.getElementById('fCodTercero').value.trim(),
|
||||
cod_contrato: document.getElementById('fCodContrato').value.trim(),
|
||||
centro_costo: document.getElementById('fCentroCosto').value.trim(),
|
||||
req_autoriza: document.getElementById('fReqAutoriza').checked,
|
||||
activa: document.getElementById('fActiva').checked,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`${BASE}/api/lab/empresas.php`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const j = await res.json();
|
||||
if (!j.ok) return showFormError(j.error || 'Error al guardar');
|
||||
_modalEmpresa.hide();
|
||||
cargarEmpresas();
|
||||
} catch(e) {
|
||||
showFormError('Error de red: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleActiva(nit) {
|
||||
await fetch(`${BASE}/api/lab/empresas.php`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({action:'toggle', nit})
|
||||
});
|
||||
cargarEmpresas();
|
||||
}
|
||||
|
||||
function showFormError(msg) { const el = document.getElementById('formError'); el.textContent = msg; el.classList.remove('d-none'); }
|
||||
function resetFormError() { document.getElementById('formError').classList.add('d-none'); }
|
||||
|
||||
// ─── Modal subgrupos ─────────────────────────────────────────
|
||||
async function abrirSubgrupos(nit, nombre) {
|
||||
document.getElementById('subNitNombre').textContent = nombre;
|
||||
document.getElementById('subNitEmpresa').value = nit;
|
||||
document.getElementById('subId').value = '';
|
||||
['subNombre','subRefSubgrupo','subCodContrato'].forEach(id => document.getElementById(id).value = '');
|
||||
document.getElementById('subTarifaId').value = '';
|
||||
document.getElementById('subError').classList.add('d-none');
|
||||
await recargarSubgrupos(nit);
|
||||
_modalSubgrupos.show();
|
||||
}
|
||||
|
||||
async function recargarSubgrupos(nit) {
|
||||
if (!nit) nit = document.getElementById('subNitEmpresa').value;
|
||||
const res = await fetch(`${BASE}/api/lab/empresa_subgrupos.php?nit_empresa=${encodeURIComponent(nit)}`);
|
||||
const j = await res.json();
|
||||
const lista = document.getElementById('listaSubgrupos');
|
||||
if (!j.subgrupos || !j.subgrupos.length) {
|
||||
lista.innerHTML = '<p class="text-muted small text-center">Sin subgrupos</p>';
|
||||
return;
|
||||
}
|
||||
lista.innerHTML = j.subgrupos.map(s => `
|
||||
<div class="sub-row">
|
||||
<div class="sub-info">
|
||||
<div class="sub-nombre">${escHtml(s.subgrupo)}</div>
|
||||
<div class="sub-meta">
|
||||
${s.tarifa_nombre ? `<span class="badge-tarifa me-2">${escHtml(s.tarifa_nombre)}</span>` : ''}
|
||||
${s.ref_subgrupo ? `Ref: ${escHtml(s.ref_subgrupo)}` : ''}
|
||||
${s.cod_contrato ? ` · Cto: ${escHtml(s.cod_contrato)}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-2" onclick="editarSubgrupo(${s.id},'${escAttr(s.subgrupo)}','${escAttr(s.tarifa_id||'')}','${escAttr(s.ref_subgrupo||'')}','${escAttr(s.cod_contrato||'')}')" title="Editar">
|
||||
<i class="fas fa-pen"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-2" onclick="eliminarSubgrupo(${s.id})" title="Eliminar">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function editarSubgrupo(id, nombre, tarifaId, ref, codCon) {
|
||||
document.getElementById('subId').value = id;
|
||||
document.getElementById('subNombre').value = nombre;
|
||||
document.getElementById('subTarifaId').value = tarifaId;
|
||||
document.getElementById('subRefSubgrupo').value = ref;
|
||||
document.getElementById('subCodContrato').value = codCon;
|
||||
document.getElementById('subNombre').focus();
|
||||
}
|
||||
|
||||
async function guardarSubgrupo() {
|
||||
const errEl = document.getElementById('subError');
|
||||
errEl.classList.add('d-none');
|
||||
const nit = document.getElementById('subNitEmpresa').value;
|
||||
const nombre = document.getElementById('subNombre').value.trim();
|
||||
if (!nombre) { errEl.textContent='El nombre es obligatorio'; errEl.classList.remove('d-none'); return; }
|
||||
|
||||
const payload = {
|
||||
action: 'save',
|
||||
id: document.getElementById('subId').value || null,
|
||||
nit_empresa: nit,
|
||||
subgrupo: nombre,
|
||||
tarifa_id: document.getElementById('subTarifaId').value,
|
||||
ref_subgrupo: document.getElementById('subRefSubgrupo').value.trim(),
|
||||
cod_contrato: document.getElementById('subCodContrato').value.trim(),
|
||||
};
|
||||
const res = await fetch(`${BASE}/api/lab/empresa_subgrupos.php`, {
|
||||
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)
|
||||
});
|
||||
const j = await res.json();
|
||||
if (!j.ok) { errEl.textContent = j.error || 'Error'; errEl.classList.remove('d-none'); return; }
|
||||
// Reset form
|
||||
document.getElementById('subId').value = '';
|
||||
['subNombre','subRefSubgrupo','subCodContrato'].forEach(id => document.getElementById(id).value='');
|
||||
document.getElementById('subTarifaId').value = '';
|
||||
recargarSubgrupos(nit);
|
||||
cargarEmpresas();
|
||||
}
|
||||
|
||||
async function eliminarSubgrupo(id) {
|
||||
if (!confirm('¿Eliminar este subgrupo?')) return;
|
||||
const nit = document.getElementById('subNitEmpresa').value;
|
||||
await fetch(`${BASE}/api/lab/empresa_subgrupos.php`, {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({action:'delete', id})
|
||||
});
|
||||
recargarSubgrupos(nit);
|
||||
cargarEmpresas();
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────
|
||||
function escHtml(s) {
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
function escAttr(s) { return escHtml(s).replace(/'/g,'''); }
|
||||
</script>
|
||||
<?php Layout::close(); ?>
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
/**
|
||||
* modules/lab_empresas/views/tarifas.php
|
||||
* CRUD del catálogo de tarifas (lab_tarifas_id).
|
||||
*/
|
||||
require_once APP_ROOT . '/config/config.php';
|
||||
Layout::open('Catálogo de Tarifas', 'fas fa-tags');
|
||||
?>
|
||||
<div class="container-fluid py-3">
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||
<div>
|
||||
<h4 class="mb-0"><i class="fas fa-tags me-2 text-primary"></i>Catálogo de Tarifas</h4>
|
||||
<small class="text-muted">IDs de tarifa usados en el motor de precios</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="<?= BASE_URL ?>/erp.php?m=lab_empresas&v=index"
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-building me-1"></i>Empresas
|
||||
</a>
|
||||
<button class="btn btn-primary btn-sm" onclick="abrirForm()">
|
||||
<i class="fas fa-plus me-1"></i>Nueva tarifa
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Tabla tarifas -->
|
||||
<div class="col-md-7">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover table-sm mb-0" id="tablaTarifas">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:60px">ID</th>
|
||||
<th>Nombre</th>
|
||||
<th class="text-end">%</th>
|
||||
<th>Origen</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbodyTarifas">
|
||||
<tr><td colspan="5" class="text-center py-4 text-muted">Cargando…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small mt-2">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Tarifas con <strong>% > 0</strong> y <strong>Origen</strong> se calculan automáticamente
|
||||
como: precio_origen × (1 + %/100).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Formulario -->
|
||||
<div class="col-md-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header py-2">
|
||||
<h6 class="mb-0 small fw-semibold" id="formTitulo">Nueva tarifa</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold small">ID numérico</label>
|
||||
<input type="number" id="tId" class="form-control form-control-sm"
|
||||
placeholder="Dejar vacío para auto-asignar" min="1">
|
||||
<div class="form-text">El ID debe coincidir con el ID Firebird si se va a migrar.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold small">Nombre <span class="text-danger">*</span></label>
|
||||
<input type="text" id="tNombre" class="form-control form-control-sm"
|
||||
placeholder="Ej: PARTICULAR, EPS SURA" maxlength="150">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold small">Porcentaje sobre tarifa origen</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="number" id="tPorcentaje" class="form-control"
|
||||
value="0" min="0" step="0.01" max="999">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
<div class="form-text">0 = precios fijos. >0 = derivada de otra tarifa.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold small">Tarifa origen</label>
|
||||
<select id="tOrigen" class="form-select form-select-sm">
|
||||
<option value="">— Precios directos —</option>
|
||||
</select>
|
||||
<div class="form-text">Solo si esta tarifa deriva de otra por porcentaje.</div>
|
||||
</div>
|
||||
<div id="tError" class="alert alert-danger d-none py-2 small mb-2"></div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-primary btn-sm flex-fill" onclick="guardarTarifa()">
|
||||
<i class="fas fa-save me-1"></i>Guardar
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="resetForm()">Cancelar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const BASE = '<?= BASE_URL ?>';
|
||||
let _tarifas = [];
|
||||
let _editId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => cargarTarifas());
|
||||
|
||||
async function cargarTarifas() {
|
||||
const res = await fetch(`${BASE}/api/lab/tarifas_id.php`);
|
||||
const j = await res.json();
|
||||
_tarifas = j.tarifas || [];
|
||||
|
||||
// Poblar select origen (excluyendo la propia tarifa en edición)
|
||||
const optsOrigen = _tarifas
|
||||
.filter(t => _editId === null || t.id != _editId)
|
||||
.map(t => `<option value="${t.id}">${escHtml(t.nombre)} (ID ${t.id})</option>`)
|
||||
.join('');
|
||||
document.getElementById('tOrigen').innerHTML = '<option value="">— Precios directos —</option>' + optsOrigen;
|
||||
|
||||
const tbody = document.getElementById('tbodyTarifas');
|
||||
if (!_tarifas.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4 text-muted">Sin tarifas registradas</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = _tarifas.map(t => `
|
||||
<tr>
|
||||
<td class="text-monospace fw-semibold text-primary">${t.id}</td>
|
||||
<td>${escHtml(t.nombre)}</td>
|
||||
<td class="text-end">${parseFloat(t.porcentaje) > 0 ? `<span class="badge bg-info text-dark">+${t.porcentaje}%</span>` : '—'}</td>
|
||||
<td>${t.tarifa_origen_nombre ? `<span class="small text-muted">${escHtml(t.tarifa_origen_nombre)}</span>` : '—'}</td>
|
||||
<td class="text-end pe-2">
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-2 me-1"
|
||||
onclick="editarTarifa(${t.id})" title="Editar">
|
||||
<i class="fas fa-pen"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-2"
|
||||
onclick="eliminarTarifa(${t.id})" title="Eliminar">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function editarTarifa(id) {
|
||||
const t = _tarifas.find(x => x.id == id);
|
||||
if (!t) return;
|
||||
_editId = id;
|
||||
document.getElementById('formTitulo').textContent = 'Editar tarifa';
|
||||
document.getElementById('tId').value = t.id;
|
||||
document.getElementById('tId').readOnly = true;
|
||||
document.getElementById('tNombre').value = t.nombre;
|
||||
document.getElementById('tPorcentaje').value = t.porcentaje;
|
||||
document.getElementById('tOrigen').value = t.tarifa_origen || '';
|
||||
document.getElementById('tError').classList.add('d-none');
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
_editId = null;
|
||||
document.getElementById('formTitulo').textContent = 'Nueva tarifa';
|
||||
document.getElementById('tId').value = '';
|
||||
document.getElementById('tId').readOnly = false;
|
||||
document.getElementById('tNombre').value = '';
|
||||
document.getElementById('tPorcentaje').value = '0';
|
||||
document.getElementById('tOrigen').value = '';
|
||||
document.getElementById('tError').classList.add('d-none');
|
||||
}
|
||||
|
||||
function abrirForm() { resetForm(); document.getElementById('tNombre').focus(); }
|
||||
|
||||
async function guardarTarifa() {
|
||||
const errEl = document.getElementById('tError');
|
||||
errEl.classList.add('d-none');
|
||||
const nombre = document.getElementById('tNombre').value.trim();
|
||||
if (!nombre) { errEl.textContent='El nombre es obligatorio'; errEl.classList.remove('d-none'); return; }
|
||||
|
||||
const payload = {
|
||||
action: 'save',
|
||||
nombre,
|
||||
porcentaje: document.getElementById('tPorcentaje').value,
|
||||
tarifa_origen:document.getElementById('tOrigen').value,
|
||||
};
|
||||
const idVal = document.getElementById('tId').value.trim();
|
||||
if (idVal) payload.id = parseInt(idVal);
|
||||
|
||||
const res = await fetch(`${BASE}/api/lab/tarifas_id.php`, {
|
||||
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)
|
||||
});
|
||||
const j = await res.json();
|
||||
if (!j.ok) { errEl.textContent = j.error || 'Error'; errEl.classList.remove('d-none'); return; }
|
||||
resetForm();
|
||||
cargarTarifas();
|
||||
}
|
||||
|
||||
async function eliminarTarifa(id) {
|
||||
if (!confirm('¿Eliminar esta tarifa? Solo es posible si no tiene precios de exámenes asociados.')) return;
|
||||
const res = await fetch(`${BASE}/api/lab/tarifas_id.php`, {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({action:'delete', id})
|
||||
});
|
||||
const j = await res.json();
|
||||
if (!j.ok) { alert(j.error || 'Error al eliminar'); return; }
|
||||
cargarTarifas();
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
</script>
|
||||
<?php Layout::close(); ?>
|
||||
Reference in New Issue
Block a user