Files
whatsapp/lab_configuracion.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 21e7c6a836 feat: integración WEBSms en configuración
- Nueva sección WEBSms en lab_configuracion.php con:
  - Switch activo/inactivo (sms_activo)
  - Campo URL base de la API (sms_url)
  - Campo API key / Bearer token (sms_api_key)
  - Campos de prueba: número + mensaje + botón Enviar
- api/lab/test_sms.php: proxy al endpoint POST /api/sms/send
  con Authorization: Bearer, valida respuesta ok/error
- save_config.php: permite guardar sms_url, sms_api_key, sms_activo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 23:27:24 -05:00

481 lines
25 KiB
PHP

<?php
/**
* lab_configuracion.php — Configuración global del laboratorio
* Solo admin.
*/
require_once 'config/config.php';
if (!defined('APP_ROOT')) { define('APP_ROOT', __DIR__); }
if (!isUserLoggedIn()) { header('Location: login.php'); exit; }
if (isEnfermero()) { header('Location: lab_dashboard.php'); exit; }
$adminNombre = $_SESSION['admin_user']['full_name'] ?? 'Admin';
// Cargar config actual
$db = Database::getInstance();
$rows= $db->fetchAll('SELECT clave, valor FROM lab_config ORDER BY clave');
$cfg = [];
foreach ($rows as $r) { $cfg[$r['clave']] = $r['valor']; }
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Configuración Lab</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="./assets/css/styles.css?v=12" rel="stylesheet">
<style>
.preview-header {
border-radius: 10px; color: #fff;
padding: 20px 24px; margin-top: 16px;
}
.preview-header h4 { margin: 0 0 2px; font-size: 15px; font-weight: 700; }
.preview-header .sub { font-size: 12px; opacity: .8; }
.preview-logo { max-height: 60px; max-width: 100px;
border-radius: 6px; background: rgba(255,255,255,.2); padding: 3px; }
.section-card { border-radius: 10px; border: 1px solid #dee2e6;
background: #fff; padding: 20px 24px; margin-bottom: 20px; }
.section-card h6 { border-bottom: 2px solid #0d6efd; padding-bottom: 8px;
margin-bottom: 16px; font-weight: 700; color: #1565c0; }
</style>
</head>
<body>
<?php
$SIDEBAR_TITLE = 'Panel Lab';
$SIDEBAR_ICON = 'fas fa-flask';
require_once __DIR__ . '/shared/components/sidebar.php';
?>
<main class="main-content">
<header class="content-header d-flex align-items-center justify-content-between">
<div>
<h1><i class="fas fa-sliders-h text-primary"></i> Configuración del Laboratorio</h1>
<small class="text-muted">Personaliza el encabezado, logo y datos que aparecen en los documentos PDF</small>
</div>
</header>
<div class="container-fluid py-3" style="max-width:860px">
<div id="toast-ok" class="alert alert-success" style="display:none;position:fixed;top:70px;right:20px;z-index:9999;min-width:260px">
<i class="fas fa-check-circle me-2"></i>Configuración guardada correctamente
</div>
<div class="row g-3">
<div class="col-lg-7">
<!-- ── Datos de la empresa ─────────────────────────── -->
<div class="section-card">
<h6><i class="fas fa-building me-1"></i>Datos de la empresa / laboratorio</h6>
<div class="mb-3">
<label class="form-label fw-semibold small">Nombre del laboratorio <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="c-empresa-nombre"
value="<?= htmlspecialchars($cfg['empresa_nombre'] ?? '') ?>"
placeholder="Ej: Laboratorio Clínico Ximena">
</div>
<div class="mb-3">
<label class="form-label fw-semibold small">Subtítulo</label>
<input type="text" class="form-control" id="c-empresa-subtitulo"
value="<?= htmlspecialchars($cfg['empresa_subtitulo'] ?? '') ?>"
placeholder="Ej: Análisis Clínicos Especializados">
</div>
<div class="row g-2 mb-3">
<div class="col-6">
<label class="form-label fw-semibold small">Teléfono</label>
<input type="text" class="form-control" id="c-empresa-telefono"
value="<?= htmlspecialchars($cfg['empresa_telefono'] ?? '') ?>"
placeholder="+57 300 000 0000">
</div>
<div class="col-6">
<label class="form-label fw-semibold small">Ciudad</label>
<input type="text" class="form-control" id="c-empresa-ciudad"
value="<?= htmlspecialchars($cfg['empresa_ciudad'] ?? '') ?>"
placeholder="Bogotá, Colombia">
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-6">
<label class="form-label fw-semibold small">Email</label>
<input type="email" class="form-control" id="c-empresa-email"
value="<?= htmlspecialchars($cfg['empresa_email'] ?? '') ?>"
placeholder="info@laboratorio.com">
</div>
<div class="col-6">
<label class="form-label fw-semibold small">Dirección</label>
<input type="text" class="form-control" id="c-empresa-direccion"
value="<?= htmlspecialchars($cfg['empresa_direccion'] ?? '') ?>"
placeholder="Calle 10 #20-30">
</div>
</div>
</div>
<!-- ── Diseño del documento ────────────────────────── -->
<div class="section-card">
<h6><i class="fas fa-paint-brush me-1"></i>Diseño del encabezado del documento</h6>
<div class="row g-3 mb-3">
<div class="col-4">
<label class="form-label fw-semibold small">Color del encabezado</label>
<input type="color" class="form-control form-control-color w-100"
id="c-doc-color"
value="<?= htmlspecialchars($cfg['doc_color'] ?? '#1565c0') ?>"
oninput="actualizarPreview()">
</div>
<div class="col-8">
<label class="form-label fw-semibold small">Logo del laboratorio</label>
<input type="file" class="form-control form-control-sm" id="c-logo-file"
accept="image/png,image/jpeg,image/gif,image/svg+xml"
onchange="cargarLogo(this)">
<div class="form-text">PNG, JPG o SVG recomendado. Máx. 200px de alto.</div>
</div>
</div>
<?php if (!empty($cfg['doc_logo_base64'])): ?>
<div class="mb-3">
<label class="form-label small fw-semibold">Logo actual:</label>
<div class="d-flex align-items-center gap-3">
<img id="c-logo-preview" src="<?= htmlspecialchars($cfg['doc_logo_base64']) ?>"
alt="Logo" style="max-height:60px;border:1px solid #dee2e6;border-radius:6px;padding:4px">
<button class="btn btn-outline-danger btn-sm" onclick="quitarLogo()">
<i class="fas fa-trash me-1"></i>Quitar logo
</button>
</div>
</div>
<?php else: ?>
<div class="mb-3" id="logo-preview-wrap" style="display:none">
<label class="form-label small fw-semibold">Vista previa del logo:</label>
<div class="d-flex align-items-center gap-3">
<img id="c-logo-preview" src="" alt="Logo"
style="max-height:60px;border:1px solid #dee2e6;border-radius:6px;padding:4px">
<button class="btn btn-outline-danger btn-sm" onclick="quitarLogo()">
<i class="fas fa-trash me-1"></i>Quitar logo
</button>
</div>
</div>
<?php endif; ?>
<input type="hidden" id="c-logo-base64" value="<?= htmlspecialchars($cfg['doc_logo_base64'] ?? '') ?>">
<div class="mb-3">
<label class="form-label fw-semibold small">Pie de página del documento</label>
<input type="text" class="form-control" id="c-doc-pie"
value="<?= htmlspecialchars($cfg['doc_pie_pagina'] ?? '') ?>"
placeholder="Ej: Documento de uso confidencial. Prohibida su reproducción.">
</div>
</div>
<!-- ── Inteligencia Artificial ────────────────────────── -->
<div class="section-card">
<div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="mb-0"><i class="fas fa-robot me-1"></i>Inteligencia Artificial — Google Gemini</h6>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="c-ia-activa" role="switch"
<?= !empty($cfg['ia_activa']) && $cfg['ia_activa'] === '1' ? 'checked' : '' ?>>
<label class="form-check-label small fw-semibold" for="c-ia-activa">Activo</label>
</div>
</div>
<p class="text-muted small mb-3">
El asistente IA del turnero usa Google Gemini Flash para responder preguntas
sobre el estado del día. Genera tu clave en
<a href="https://aistudio.google.com/app/apikey" target="_blank" rel="noopener">Google AI Studio</a>.
</p>
<div class="mb-2">
<label class="form-label fw-semibold small">Token de API (Gemini Flash)</label>
<div class="input-group">
<input type="password" class="form-control" id="c-gemini-key"
value="<?= htmlspecialchars($cfg['gemini_api_key'] ?? '') ?>"
placeholder="AIza…"
autocomplete="off">
<button class="btn btn-outline-secondary" type="button"
onclick="this.previousElementSibling.type = this.previousElementSibling.type === 'password' ? 'text' : 'password'">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-outline-primary" type="button" id="btn-probar-ia" onclick="probarIA()">
<i class="fas fa-plug me-1"></i>Probar
</button>
</div>
<div id="ia-test-result" class="form-text mt-1"></div>
</div>
</div>
<!-- ── WEBSms ──────────────────────────────────────────── -->
<div class="section-card">
<div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="mb-0"><i class="fas fa-sms me-1"></i>Integración WEBSms</h6>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="c-sms-activo" role="switch"
<?= !empty($cfg['sms_activo']) && $cfg['sms_activo'] === '1' ? 'checked' : '' ?>>
<label class="form-check-label small fw-semibold" for="c-sms-activo">Activo</label>
</div>
</div>
<p class="text-muted small mb-3">
Permite enviar SMS a pacientes desde el sistema. Requiere una cuenta en WEBSms.
</p>
<div class="mb-2">
<label class="form-label fw-semibold small">URL base de la API</label>
<input type="url" class="form-control" id="c-sms-url"
value="<?= htmlspecialchars($cfg['sms_url'] ?? '') ?>"
placeholder="https://api.websms.com">
<div class="form-text">Sin barra final. El sistema agrega <code>/api/sms/send</code>.</div>
</div>
<div class="mb-2">
<label class="form-label fw-semibold small">API Key (Bearer token)</label>
<div class="input-group">
<input type="password" class="form-control" id="c-sms-key"
value="<?= htmlspecialchars($cfg['sms_api_key'] ?? '') ?>"
placeholder="sk_…"
autocomplete="off">
<button class="btn btn-outline-secondary" type="button"
onclick="this.previousElementSibling.type = this.previousElementSibling.type === 'password' ? 'text' : 'password'">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div class="mb-2">
<label class="form-label fw-semibold small">Probar envío</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-phone"></i></span>
<input type="tel" class="form-control" id="c-sms-test-num"
placeholder="573001234567" maxlength="15">
<input type="text" class="form-control" id="c-sms-test-msg"
placeholder="Mensaje de prueba" maxlength="160" value="Prueba de conexión WEBSms">
<button class="btn btn-outline-primary" type="button" id="btn-probar-sms" onclick="probarSms()">
<i class="fas fa-paper-plane me-1"></i>Enviar
</button>
</div>
<div id="sms-test-result" class="form-text mt-1"></div>
</div>
</div>
<button class="btn btn-primary btn-lg w-100" onclick="guardar()">
<i class="fas fa-save me-2"></i>Guardar configuración
</button>
</div>
<!-- ── Vista previa ──────────────────────────────── -->
<div class="col-lg-5">
<div class="section-card" style="position:sticky;top:80px">
<h6><i class="fas fa-eye me-1"></i>Vista previa del encabezado</h6>
<div id="preview-box" class="preview-header"
style="background: <?= htmlspecialchars($cfg['doc_color'] ?? '#1565c0') ?>">
<div style="display:flex;align-items:flex-start;gap:12px">
<div id="preview-logo-wrap" style="display:<?= !empty($cfg['doc_logo_base64']) ? '' : 'none' ?>">
<img id="preview-logo" src="<?= htmlspecialchars($cfg['doc_logo_base64'] ?? '') ?>"
class="preview-logo" alt="Logo">
</div>
<div style="flex:1">
<h4 id="preview-nombre"><?= htmlspecialchars($cfg['empresa_nombre'] ?? 'Nombre del laboratorio') ?></h4>
<div class="sub" id="preview-subtitulo"><?= htmlspecialchars($cfg['empresa_subtitulo'] ?? 'Subtítulo') ?></div>
<div class="sub" style="margin-top:2px" id="preview-contacto">
<?php
$c = array_filter([
$cfg['empresa_direccion'] ?? '',
$cfg['empresa_ciudad'] ?? '',
$cfg['empresa_telefono'] ?? '',
]);
echo htmlspecialchars(implode(' · ', $c));
?>
</div>
<h5 style="margin:8px 0 0;border-top:1px solid rgba(255,255,255,.3);padding-top:8px;font-size:14px">
Nombre del formulario
</h5>
</div>
</div>
</div>
<div class="mt-3 p-3 bg-light rounded small text-muted">
<i class="fas fa-info-circle me-1"></i>
Este encabezado aparecerá en todos los documentos PDF generados.
Cada formulario puede tener su propio encabezado desde el editor de formularios.
</div>
<div class="mt-3">
<a href="ver_formulario_enviado.php?id=<?php
$uno = $db->fetch('SELECT id FROM lab_form_envios WHERE estado IN (\'firmado\',\'completado\') ORDER BY id DESC LIMIT 1');
echo $uno ? (int)$uno['id'] : '0';
?>" target="_blank" class="btn btn-outline-primary btn-sm w-100">
<i class="fas fa-external-link-alt me-1"></i>Ver ejemplo de documento
</a>
</div>
</div>
</div>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const $ = id => document.getElementById(id);
function actualizarPreview() {
const color = $('c-doc-color').value;
const nombre = $('c-empresa-nombre').value || 'Nombre del laboratorio';
const sub = $('c-empresa-subtitulo').value || 'Subtítulo';
const tel = $('c-empresa-telefono').value;
const dir = $('c-empresa-direccion').value;
const ciudad = $('c-empresa-ciudad').value;
$('preview-box').style.background = color;
$('preview-nombre').textContent = nombre;
$('preview-subtitulo').textContent = sub;
const ctc = [dir, ciudad, tel].filter(Boolean).join(' · ');
$('preview-contacto').textContent = ctc;
}
// Actualizar preview al escribir
['c-empresa-nombre','c-empresa-subtitulo','c-empresa-telefono',
'c-empresa-direccion','c-empresa-ciudad'].forEach(id => {
$(id)?.addEventListener('input', actualizarPreview);
});
function cargarLogo(input) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
const b64 = e.target.result;
$('c-logo-base64').value = b64;
$('c-logo-preview').src = b64;
$('preview-logo').src = b64;
const wrap = $('logo-preview-wrap') || $('c-logo-preview').parentElement;
if (wrap) wrap.style.display = '';
$('preview-logo-wrap').style.display = '';
};
reader.readAsDataURL(file);
}
function quitarLogo() {
$('c-logo-base64').value = '';
$('c-logo-preview').src = '';
$('preview-logo').src = '';
$('preview-logo-wrap').style.display = 'none';
const wrap = $('logo-preview-wrap');
if (wrap) wrap.style.display = 'none';
$('c-logo-file').value = '';
}
async function guardar() {
const payload = {
empresa_nombre: $('c-empresa-nombre').value.trim(),
empresa_subtitulo: $('c-empresa-subtitulo').value.trim(),
empresa_telefono: $('c-empresa-telefono').value.trim(),
empresa_email: $('c-empresa-email').value.trim(),
empresa_direccion: $('c-empresa-direccion').value.trim(),
empresa_ciudad: $('c-empresa-ciudad').value.trim(),
doc_color: $('c-doc-color').value,
doc_logo_base64: $('c-logo-base64').value,
doc_pie_pagina: $('c-doc-pie').value.trim(),
gemini_api_key: $('c-gemini-key').value.trim(),
ia_activa: $('c-ia-activa').checked ? '1' : '0',
sms_url: $('c-sms-url').value.trim().replace(/\/$/, ''),
sms_api_key: $('c-sms-key').value.trim(),
sms_activo: $('c-sms-activo').checked ? '1' : '0',
};
if (!payload.empresa_nombre) {
$('c-empresa-nombre').focus();
$('c-empresa-nombre').classList.add('is-invalid');
return;
}
const btn = document.querySelector('button[onclick="guardar()"]');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Guardando…';
try {
const r = await fetch('api/lab/save_config.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const d = await r.json();
if (d.success) {
const t = $('toast-ok');
t.style.display = 'block';
setTimeout(() => { t.style.display = 'none'; }, 3500);
} else {
alert(d.error || 'Error al guardar');
}
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-save me-2"></i>Guardar configuración';
}
}
async function probarSms() {
const url = $('c-sms-url').value.trim().replace(/\/$/, '');
const key = $('c-sms-key').value.trim();
const num = $('c-sms-test-num').value.trim();
const msg = $('c-sms-test-msg').value.trim();
const res = document.getElementById('sms-test-result');
const btn = document.getElementById('btn-probar-sms');
if (!url) { res.innerHTML = '<span class="text-danger">Ingresa la URL base primero.</span>'; return; }
if (!key) { res.innerHTML = '<span class="text-danger">Ingresa la API key primero.</span>'; return; }
if (!num) { res.innerHTML = '<span class="text-danger">Ingresa un número de destino.</span>'; return; }
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
res.innerHTML = '';
try {
const r = await fetch('api/lab/test_sms.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, key, numero: num, mensaje: msg || 'Prueba de conexión WEBSms' }),
});
const d = await r.json();
if (d.ok) {
res.innerHTML = `<span class="text-success"><i class="fas fa-check-circle me-1"></i>SMS enviado — ID: <code>${d.id || '—'}</code></span>`;
} else {
res.innerHTML = `<span class="text-danger"><i class="fas fa-times-circle me-1"></i>${d.error || 'Error desconocido'}</span>`;
}
} catch(_) {
res.innerHTML = '<span class="text-danger"><i class="fas fa-times-circle me-1"></i>No se pudo conectar.</span>';
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-paper-plane me-1"></i>Enviar';
}
}
async function probarIA() {
const key = $('c-gemini-key').value.trim();
const res = document.getElementById('ia-test-result');
const btn = document.getElementById('btn-probar-ia');
if (!key) { res.innerHTML = '<span class="text-danger">Ingresa el token primero.</span>'; return; }
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
res.innerHTML = '';
try {
const r = await fetch('api/lab/test_gemini.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key }),
});
const d = await r.json();
if (d.ok) {
res.innerHTML = '<span class="text-success"><i class="fas fa-check-circle me-1"></i>Conexión exitosa — ' + (d.modelo || 'Gemini') + '</span>';
} else {
res.innerHTML = '<span class="text-danger"><i class="fas fa-times-circle me-1"></i>' + (d.error || 'Error de conexión') + '</span>';
}
} catch(_) {
res.innerHTML = '<span class="text-danger"><i class="fas fa-times-circle me-1"></i>No se pudo conectar.</span>';
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-plug me-1"></i>Probar';
}
}
</script>
<script src="assets/js/lab-sidebar.js"></script>
</body>
</html>