Files
whatsapp/modules/turnero/api/create_solicitud.php
Lizandro GuarnizoandClaude Sonnet 4.6 8e1f46904c Registrar quién crea cada consentimiento del turnero para el pie "Enviado por"
turnero_consentimientos nunca guardó quién lo generó; el pie del documento
mostraba el nombre de quien tuviera sesión abierta al verlo, no de quien
realmente lo atendió (por eso siempre aparecía el mismo usuario). Se agrega
la columna creado_por, se llena en los 6 puntos donde se crea un registro
(recepción, envío por WhatsApp, anexar formulario extra, autocreación por
examen/lugar), y ver_formulario_enviado.php ahora prioriza ese dato real
sobre el usuario de la sesión actual. Los documentos ya firmados antes de
este cambio siguen sin ese dato y usan el fallback anterior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-03 12:50:41 -05:00

414 lines
18 KiB
PHP

<?php
/**
* POST /modules/turnero/api/create_solicitud.php
* Crea la solicitud interna de un turno: vincula paciente, lugar y exámenes.
* Además devuelve qué consentimientos serán necesarios (sin enviarlos aún).
* Requiere login + módulo turnero.
*
* Body JSON:
* turno_id int requerido
* paciente_id int requerido
* lugar_id int requerido
* exam_tipo_ids int[] requerido (al menos 1)
* total_cobrado float opcional
* metodo_pago string opcional
* observaciones string opcional
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
requireTurnero();
$datos = inputJson();
$turnoId = isset($datos['turno_id']) ? (int) $datos['turno_id'] : 0;
$pacienteId = isset($datos['paciente_id']) ? (int) $datos['paciente_id'] : 0;
$lugarId = isset($datos['lugar_id']) ? (int) $datos['lugar_id'] : 0;
$examIds = isset($datos['exam_tipo_ids']) && is_array($datos['exam_tipo_ids'])
? array_filter(array_map('intval', $datos['exam_tipo_ids'])) : [];
$total = isset($datos['total_cobrado']) && $datos['total_cobrado'] !== null
? (float) $datos['total_cobrado'] : null;
$metodoPago = isset($datos['metodo_pago']) ? trim((string) $datos['metodo_pago']) : null;
$numeroRecibo = !empty($datos['numero_recibo']) ? trim((string) $datos['numero_recibo']) : null;
$obs = isset($datos['observaciones']) ? trim((string) $datos['observaciones']) : null;
$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;
$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;
$combRefs = [];
if ($metodoPago === 'combinado' && isset($datos['pagos_detalle']) && is_array($datos['pagos_detalle'])) {
$pd = $datos['pagos_detalle'];
$pagosDetalle = json_encode([
'efectivo' => max(0, (float)($pd['efectivo'] ?? 0)),
'transferencia' => max(0, (float)($pd['transferencia'] ?? 0)),
'tarjeta' => max(0, (float)($pd['tarjeta'] ?? 0)),
'bono' => max(0, (float)($pd['bono'] ?? 0)),
]);
if (!empty($pd['ref_transferencia'])) $combRefs['transferencia'] = trim((string)$pd['ref_transferencia']);
if (!empty($pd['ref_tarjeta'])) $combRefs['tarjeta'] = trim((string)$pd['ref_tarjeta']);
}
if ($numOrden === '') $numOrden = null;
// ── Validaciones ──────────────────────────────────────────────
if ($turnoId <= 0) jsonError('turno_id inválido.');
if ($pacienteId <= 0) jsonError('paciente_id inválido.');
if ($lugarId <= 0) jsonError('lugar_id inválido.');
if (empty($examIds) && !$soloMuestras) jsonError('Debe seleccionar al menos un examen.');
$metodosValidos = ['efectivo', 'transferencia', 'tarjeta', 'eps', 'cortesia', 'bono', 'combinado', ''];
if ($metodoPago && !in_array($metodoPago, $metodosValidos, true)) {
jsonError('metodo_pago inválido.');
}
$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 {
// Verificar que el turno existe y está en recepción
$stmt = $pdo->prepare(
"SELECT id, estado FROM turnero_turnos WHERE id = ? FOR UPDATE"
);
$stmt->execute([$turnoId]);
$turno = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$turno) {
$pdo->rollBack();
jsonError('Turno no encontrado.', 404);
}
if (!in_array($turno['estado'], ['en_recepcion', 'espera'], true)) {
$pdo->rollBack();
jsonError("El turno está en estado '{$turno['estado']}' y no se puede editar su solicitud.", 422);
}
// Si el turno fue seleccionado sin llamarlo formalmente, promoverlo a en_recepcion ahora
if ($turno['estado'] === 'espera') {
$pdo->prepare(
"UPDATE turnero_turnos
SET estado = 'en_recepcion',
llamado_recepcion_at = COALESCE(llamado_recepcion_at, NOW()),
inicio_recepcion_at = COALESCE(inicio_recepcion_at, NOW()),
atendido_recepcion_por = COALESCE(atendido_recepcion_por, ?)
WHERE id = ?"
)->execute([adminId(), $turnoId]);
}
// Verificar que el lugar existe
$stmt = $pdo->prepare("SELECT id FROM turnero_lugares WHERE id = ? AND activo = 1");
$stmt->execute([$lugarId]);
if (!$stmt->fetch()) {
$pdo->rollBack();
jsonError('Lugar destino no encontrado o inactivo.', 404);
}
// Verificar que todos los exam_tipo_ids existen (se omite si solo_muestras y sin exámenes)
if (!empty($examIds)) {
$in = implode(',', array_fill(0, count($examIds), '?'));
$stmt = $pdo->prepare("SELECT COUNT(*) FROM exam_tipos WHERE id IN ({$in}) AND activo = 1");
$stmt->execute($examIds);
if ((int)$stmt->fetchColumn() !== count($examIds)) {
$pdo->rollBack();
jsonError('Uno o más exámenes no existen o están inactivos.', 422);
}
}
// ── Insertar o reemplazar solicitud (DELETE + INSERT para poder re-guardar) ──
$stmt = $pdo->prepare("SELECT id FROM turnero_solicitudes WHERE turno_id = ?");
$stmt->execute([$turnoId]);
$solAnterior = $stmt->fetch(PDO::FETCH_ASSOC);
if ($solAnterior) {
// Eliminar ítemes anteriores (CASCADE borrará los turnero_examen_items)
$pdo->prepare("DELETE FROM turnero_solicitudes WHERE turno_id = ?")->execute([$turnoId]);
}
$stmt = $pdo->prepare(
"INSERT INTO turnero_solicitudes
(turno_id, paciente_id, lugar_id, numero_orden, total_cobrado, metodo_pago, numero_recibo, 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, $numeroRecibo, $pagosDetalle, $obs ?: null, $embarazada, $soloMuestras, $medicoId,
$nitEmpresa, $subgrupoId, $autorizacion, $diagPpal, $itemsPrecio, adminId(),
]);
$solicitudId = (int) $pdo->lastInsertId();
// ── Insertar ítemes de examen ──────────────────────────────
$stmtItem = $pdo->prepare(
"INSERT INTO turnero_examen_items (solicitud_id, exam_tipo_id) VALUES (?, ?)"
);
foreach ($examIds as $examId) {
$stmtItem->execute([$solicitudId, $examId]);
}
// ── Consentimientos: solo los de examen (los de lugar se crean al llegar a toma de muestras) ──
$consentimientosRequeridos = [];
if (!empty($examIds)) {
$ph = implode(',', array_fill(0, count($examIds), '?'));
$stmt = $pdo->prepare(
"SELECT DISTINCT f.id AS formulario_id, f.nombre AS formulario_nombre
FROM lab_formularios f
INNER JOIN exam_tipo_consentimientos etc ON etc.formulario_id = f.id
WHERE etc.exam_tipo_id IN ($ph)
AND f.is_active = 1
ORDER BY f.nombre ASC"
);
$stmt->execute($examIds);
$consentimientosRequeridos = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmtCreateConsent = $pdo->prepare(
"INSERT IGNORE INTO turnero_consentimientos (turno_id, formulario_id, token, estado, creado_por)
VALUES (?, ?, UUID(), 'pendiente', ?)"
);
$stmtRead = $pdo->prepare(
"SELECT id, token, estado FROM turnero_consentimientos
WHERE turno_id = ? AND formulario_id = ?"
);
foreach ($consentimientosRequeridos as &$c) {
$stmtCreateConsent->execute([$turnoId, $c['formulario_id'], adminId()]);
$stmtRead->execute([$turnoId, $c['formulario_id']]);
$row = $stmtRead->fetch(PDO::FETCH_ASSOC);
$c['id'] = $row['id'] ?? null;
$c['token'] = $row['token'] ?? null;
$c['estado'] = $row['estado'] ?? 'pendiente';
$c['turno_id'] = $turnoId;
$c['origen_lugar_id'] = null; // es de examen, no de lugar
}
unset($c);
}
// ── Auto-crear muestras por tipo_muestra de los exámenes ──
try {
if (!$soloMuestras && !empty($examIds)) {
$ph = implode(',', array_fill(0, count($examIds), '?'));
$stmtTm = $pdo->prepare(
"SELECT DISTINCT COALESCE(NULLIF(TRIM(tipo_muestra),''), codigo) AS tipo_key
FROM exam_tipos WHERE id IN ($ph) AND activo = 1"
);
$stmtTm->execute($examIds);
$stmtIm = $pdo->prepare(
"INSERT IGNORE INTO turnero_muestras (solicitud_id, tipo_muestra) VALUES (?, ?)"
);
foreach ($stmtTm->fetchAll(PDO::FETCH_COLUMN) as $tipoKey) {
$stmtIm->execute([$solicitudId, $tipoKey]);
}
} elseif ($soloMuestras) {
$pdo->prepare(
"INSERT IGNORE INTO turnero_muestras (solicitud_id, tipo_muestra) VALUES (?, 'MUESTRA')"
)->execute([$solicitudId]);
}
} catch (\Throwable $_) {
// Tabla aún no existe (migración pendiente) — continuar sin muestras
}
// ── Crear lab_recepciones + lab_relaciones + lab_pagos ───────
// Envuelto en try/catch para no romper el flujo si la migración 11
// aún no se ha ejecutado en este entorno.
$recepcionId = null;
try {
$operador = $_SESSION['admin_user']['username'] ?? ('u' . adminId());
// Subgrupo nombre (para campo subgrupo en recepcion)
$subgrupoNombre = null;
if ($subgrupoId) {
$stS = $pdo->prepare("SELECT subgrupo FROM lab_empresa_subgrupos WHERE id = ?");
$stS->execute([$subgrupoId]);
$subgrupoNombre = $stS->fetchColumn() ?: null;
}
// Tipo usuario de la empresa
$tipoUsuario = null;
if ($nitEmpresa) {
$stE = $pdo->prepare("SELECT tipo_usuario FROM lab_empresas WHERE nit = ?");
$stE->execute([$nitEmpresa]);
$tipoUsuario = $stE->fetchColumn() ?: null;
}
// Siguiente num_factura para registros nuevos (es_historico=0)
// FOR UPDATE garantiza secuencia correcta bajo concurrencia
$nextFac = (int)$pdo->query(
"SELECT COALESCE(MAX(num_factura), 0) + 1 FROM lab_recepciones WHERE es_historico = 0 FOR UPDATE"
)->fetchColumn();
// Calcular valor_desc desde el snapshot de precios
$valorTotal = $total ?? 0;
$valorDesc = 0;
if ($itemsPrecio) {
$snap = json_decode($itemsPrecio, true) ?? [];
$subtotalS = array_sum(array_column($snap, 'valor_final'));
$valorDesc = max(0, round($subtotalS - $valorTotal, 2));
}
$stR = $pdo->prepare(
"INSERT INTO lab_recepciones
(solicitud_id, paciente_id, medico_id, nit_empresa, subgrupo,
fecha_recepcion, hora_inicio, num_factura,
valor_total, valor_desc, diag_ppal, tipo_usuario, autorizacion,
usuario, es_historico)
VALUES (?, ?, ?, ?, ?, CURDATE(), CURTIME(), ?,
?, ?, ?, ?, ?, ?, 0)"
);
$stR->execute([
$solicitudId, $pacienteId, $medicoId, $nitEmpresa, $subgrupoNombre,
$nextFac,
$valorTotal, $valorDesc, $diagPpal, $tipoUsuario, $autorizacion,
$operador,
]);
$recepcionId = (int)$pdo->lastInsertId();
// lab_relaciones: un registro por examen
if (!empty($examIds)) {
$ph = implode(',', array_fill(0, count($examIds), '?'));
$stEx = $pdo->prepare("SELECT id, codigo FROM exam_tipos WHERE id IN ($ph)");
$stEx->execute($examIds);
$codigos = $stEx->fetchAll(PDO::FETCH_ASSOC);
// Mapa exam_tipo_id → precio desde el snapshot
$precioMap = [];
if ($itemsPrecio) {
foreach (json_decode($itemsPrecio, true) ?? [] as $item) {
$precioMap[(int)$item['exam_tipo_id']] = (float)($item['valor_final'] ?? 0);
}
}
$stRel = $pdo->prepare(
"INSERT INTO lab_relaciones (recepcion_id, cod_examen_legacy, exam_tipo_id, precio)
VALUES (?, ?, ?, ?)"
);
foreach ($codigos as $ex) {
$stRel->execute([
$recepcionId,
$ex['codigo'],
(int)$ex['id'],
$precioMap[(int)$ex['id']] ?? 0,
]);
}
}
// lab_pagos + lab_pagos_det si hay cobro
if ($metodoPago && ($valorTotal > 0 || $metodoPago === 'cortesia')) {
$stP = $pdo->prepare(
"INSERT INTO lab_pagos (recepcion_id, valor, fecha, usuario)
VALUES (?, ?, CURDATE(), ?)"
);
$stP->execute([$recepcionId, $valorTotal, $operador]);
$pagoId = (int)$pdo->lastInsertId();
$stD = $pdo->prepare(
"INSERT INTO lab_pagos_det (pago_id, tipo_pago, valor, num_doc) VALUES (?, ?, ?, ?)"
);
if ($metodoPago === 'combinado' && $pagosDetalle) {
foreach (json_decode($pagosDetalle, true) ?? [] as $tipo => $val) {
if ((float)$val > 0) {
$ref = $combRefs[$tipo] ?? null;
$stD->execute([$pagoId, $tipo, (float)$val, $ref]);
}
}
} else {
$stD->execute([$pagoId, $metodoPago, $valorTotal, $numeroRecibo]);
}
}
} catch (\Throwable $eRec) {
// Si falla (migración no aplicada, constraint, etc.) solo logueamos.
// La solicitud principal ya está lista; no la revertimos por esto.
error_log('[create_solicitud] lab_recepciones error: ' . $eRec->getMessage());
$recepcionId = null;
}
$pdo->commit();
notificarSSE(obtenerOCrearSesionHoy());
// Recuperar solicitud completa
$stmt = $pdo->prepare(
"SELECT s.id, s.turno_id, s.paciente_id, s.lugar_id, s.total_cobrado,
s.metodo_pago, s.observaciones, s.creado_at,
p.nombre_completo AS paciente_nombre,
p.numero_documento AS paciente_documento,
l.nombre AS lugar_nombre
FROM turnero_solicitudes s
LEFT JOIN lab_pacientes p ON p.id = s.paciente_id
LEFT JOIN turnero_lugares l ON l.id = s.lugar_id
WHERE s.id = ?"
);
$stmt->execute([$solicitudId]);
$solicitud = $stmt->fetch(PDO::FETCH_ASSOC);
// Bandeja WhatsApp — actualizar rips_examenes_pendientes (fuera de la transacción principal)
if ($recepcionId && !empty($examIds) && ($solicitud['paciente_documento'] ?? '')) {
try {
$numDoc = $solicitud['paciente_documento'];
$ph2 = implode(',', array_fill(0, count($examIds), '?'));
$stEx2 = $pdo->prepare(
"SELECT COALESCE(codigo_legacy, codigo) AS cod_rips FROM exam_tipos WHERE id IN ($ph2)"
);
$stEx2->execute($examIds);
$codRips = array_column($stEx2->fetchAll(PDO::FETCH_ASSOC), 'cod_rips');
$docidmedico = '';
if ($medicoId) {
$stM = $pdo->prepare("SELECT docidmedico FROM medicos WHERE id = ? LIMIT 1");
$stM->execute([$medicoId]);
$docidmedico = (string)($stM->fetchColumn() ?: '');
}
$meta = [
'medico_docidmedico' => $docidmedico,
'nit_empresa' => $nitEmpresa,
'diagnostico_cod' => $diagPpal,
'valor_total' => $valorTotal,
'recepcion_id' => $recepcionId,
'hora' => date('H:i'),
];
$datos = array_map(fn($cod) => ['cod_examen' => $cod] + $meta, $codRips);
// Consumir el cache: solo eliminar, no reinsertar, para que no recargue en una segunda visita
$pdo->prepare(
"DELETE FROM rips_examenes_pendientes WHERE numero_documento = ? AND DATE(created_at) = CURDATE()"
)->execute([$numDoc]);
} catch (\Throwable $eRips) {
error_log('[create_solicitud] rips_examenes_pendientes: ' . $eRips->getMessage());
}
}
jsonOk([
'solicitud' => $solicitud,
'recepcion_id' => $recepcionId,
'consentimientos_requeridos' => $consentimientosRequeridos,
], 'Solicitud guardada correctamente');
} catch (\Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
jsonError('Error al crear solicitud: ' . $e->getMessage(), 500);
}