Files
whatsapp/modules/turnero/api/create_solicitud.php
T

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)
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']]);
$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);
}