max(0, (float)($pd['efectivo'] ?? 0)), 'transferencia' => max(0, (float)($pd['transferencia'] ?? 0)), 'tarjeta' => max(0, (float)($pd['tarjeta'] ?? 0)), ]); } 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(); $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, 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, $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) VALUES (?, ?, ?)" ); if ($metodoPago === 'combinado' && $pagosDetalle) { foreach (json_decode($pagosDetalle, true) ?? [] as $tipo => $val) { if ((float)$val > 0) { $stD->execute([$pagoId, $tipo, (float)$val]); } } } else { $stD->execute([$pagoId, $metodoPago, $valorTotal]); } } } 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); 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); }