feat: restricción de exámenes por género del paciente

- DB: columna genero_permitido en exam_tipos (M/F/NULL)
  GX,FV → solo femenino | PSA,FPSA → solo masculino
- recepcion.php: TomSelect muestra badge rojo y deshabilita opciones
  incompatibles al seleccionar paciente; elimina los ya agregados
- create_solicitud.php: validación backend 422 si hay conflicto género/examen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-17 16:45:39 -05:00
co-authored by Claude Sonnet 4.6
parent b280dfcb7a
commit 3eaade80ff
2 changed files with 49 additions and 5 deletions
+23
View File
@@ -63,6 +63,29 @@ if ($metodoPago && !in_array($metodoPago, $metodosValidos, true)) {
}
$pdo = db();
// Validar restricción de género antes de abrir transacción
if (!empty($examIds)) {
$stPac = $pdo->prepare("SELECT genero FROM lab_pacientes WHERE id = ? LIMIT 1");
$stPac->execute([$pacienteId]);
$genPac = strtoupper(trim((string)($stPac->fetchColumn() ?: '')));
if ($genPac) {
$ph = implode(',', array_fill(0, count($examIds), '?'));
$stGp = $pdo->prepare(
"SELECT codigo, nombre, genero_permitido FROM exam_tipos
WHERE id IN ($ph) AND genero_permitido IS NOT NULL AND genero_permitido <> ?"
);
$stGp->execute(array_merge($examIds, [$genPac]));
$incompatibles = $stGp->fetchAll(PDO::FETCH_ASSOC);
if ($incompatibles) {
$nombres = implode(', ', array_map(fn($r) => $r['codigo'], $incompatibles));
$sexo = $genPac === 'M' ? 'masculino' : 'femenino';
jsonError("Examen(es) no disponibles para paciente $sexo: $nombres", 422);
}
}
}
$pdo->beginTransaction();
try {
+26 -5
View File
@@ -82,7 +82,7 @@ try {
$lugarMuestras = $lugaresDestino[0] ?? null;
$examenes = $pdo->query(
"SELECT et.id, et.codigo, et.nombre, et.categoria, et.cups,
"SELECT et.id, et.codigo, et.nombre, et.categoria, et.cups, et.genero_permitido,
IF(COUNT(etc.formulario_id) > 0, 1, 0) AS tiene_consentimiento
FROM exam_tipos et
LEFT JOIN exam_tipo_consentimientos etc ON etc.exam_tipo_id = et.id
@@ -732,7 +732,8 @@ document.addEventListener('DOMContentLoaded', function() {
<option value="<?= (int)$ex['id'] ?>"
data-consent="<?= (int)$ex['tiene_consentimiento'] ?>"
data-cups="<?= htmlspecialchars($ex['cups'] ?? '') ?>"
data-codigo="<?= htmlspecialchars($ex['codigo'] ?? '') ?>">
data-codigo="<?= htmlspecialchars($ex['codigo'] ?? '') ?>"
data-genero="<?= htmlspecialchars($ex['genero_permitido'] ?? '') ?>">
<?= htmlspecialchars($ex['codigo']) ?> — <?= htmlspecialchars($ex['nombre']) ?><?= $ex['cups'] ? ' ' . htmlspecialchars($ex['cups']) : '' ?>
</option>
<?php endforeach; ?>
@@ -1155,10 +1156,14 @@ document.addEventListener('DOMContentLoaded', () => {
render: {
option(data, escape) {
const consent = data['data-consent'] == '1' || data.consent == '1';
const cups = data['data-cups'] || data.cups || '';
return `<div class="d-flex align-items-center justify-content-between">
const cups = data['data-cups'] || data.cups || '';
const gp = (data['data-genero'] || '').toUpperCase();
const pg = (pacienteActivo?.genero || '').toUpperCase();
const blocked = gp && pg && gp !== pg;
const gpLabel = gp === 'M' ? '♂ Solo hombres' : gp === 'F' ? '♀ Solo mujeres' : '';
return `<div class="d-flex align-items-center justify-content-between${blocked ? ' opacity-50' : ''}">
<span>${escape(data.text)}${cups ? `<span class="ts-exam-cups-drop">${escape(cups)}</span>` : ''}</span>
${consent ? '<span class="ts-consent-badge"><i class="fas fa-file-signature me-1"></i>Consentimiento</span>' : ''}
${blocked ? `<span class="ts-consent-badge bg-danger">${gpLabel}</span>` : consent ? '<span class="ts-consent-badge"><i class="fas fa-file-signature me-1"></i>Consentimiento</span>' : ''}
</div>`;
},
item(data, escape) {
@@ -1618,6 +1623,22 @@ function seleccionarPaciente(pac) {
// Consultar exámenes recientes en RIPS (últimos 5 min)
const cedula = (pac.numero_documento || pac.documento || '').toString().trim();
if (cedula) consultarExamenesRips(cedula);
filtrarExamenesPorGenero();
}
function filtrarExamenesPorGenero() {
if (!examTS) return;
const genero = (pacienteActivo?.genero || '').toUpperCase();
const bloqueados = [];
Object.values(examTS.options).forEach(opt => {
const gp = (opt['data-genero'] || '').toUpperCase();
const incompatible = !!(gp && genero && gp !== genero);
opt.disabled = incompatible;
if (incompatible && examTS.items.includes(String(opt.value))) bloqueados.push(String(opt.value));
});
bloqueados.forEach(id => examTS.removeItem(id, true));
if (bloqueados.length) recalcularPrecios();
examTS.refreshOptions(false);
}
// ── Exámenes desde RIPS ───────────────────────────────────────