800) jsonError('Pregunta demasiado larga', 400); $pdo = db(); // ── Clave Gemini ────────────────────────────────────────────── $row = $pdo->query("SELECT valor FROM lab_config WHERE clave = 'gemini_api_key' LIMIT 1")->fetch(); $apiKey = trim($row['valor'] ?? ''); if (!$apiKey) jsonError('Token de IA no configurado. Ve a Configuración del laboratorio.', 503); // ── Contexto del día ────────────────────────────────────────── $hoy = date('Y-m-d'); $ctx = "Hoy es {$hoy}.\n"; $sesRow = $pdo->prepare("SELECT id, inicio_at, fin_at FROM turnero_sesiones WHERE fecha = ? LIMIT 1"); $sesRow->execute([$hoy]); $ses = $sesRow->fetch(PDO::FETCH_ASSOC); if (!$ses) { $ctx .= "No hay sesión de atención abierta hoy.\n"; } else { $sesId = (int)$ses['id']; $ctx .= "Sesión " . ($ses['fin_at'] ? 'cerrada' : 'abierta') . ", inicio: " . ($ses['inicio_at'] ?? 'pendiente') . ".\n"; // Resumen de estados $stmt = $pdo->prepare("SELECT estado, COUNT(*) AS n FROM turnero_turnos WHERE sesion_id = ? GROUP BY estado"); $stmt->execute([$sesId]); $stats = $stmt->fetchAll(PDO::FETCH_ASSOC); $total = array_sum(array_column($stats, 'n')); $ctx .= "Turnos hoy: {$total} total. "; foreach ($stats as $s) { $ctx .= "{$s['estado']}: {$s['n']}. "; } $ctx .= "\n"; // Tiempos promedio globales $tRow = $pdo->prepare( "SELECT ROUND(AVG(TIMESTAMPDIFF(SECOND, creado_at, COALESCE(inicio_recepcion_at, NOW())) / 60), 1) AS espera_prom, ROUND(AVG(TIMESTAMPDIFF(SECOND, inicio_recepcion_at, fin_recepcion_at) / 60), 1) AS recepcion_prom, ROUND(AVG(TIMESTAMPDIFF(SECOND, inicio_lugar_at, COALESCE(fin_lugar_at, NOW())) / 60), 1) AS servicio_prom, ROUND(AVG(TIMESTAMPDIFF(SECOND, creado_at, COALESCE(fin_lugar_at, NOW())) / 60), 1) AS total_prom FROM turnero_turnos WHERE sesion_id = ?" ); $tRow->execute([$sesId]); $tiempos = $tRow->fetch(PDO::FETCH_ASSOC); $ctx .= "Tiempos promedio — espera: " . ($tiempos['espera_prom'] ?? '—') . "min, "; $ctx .= "recepción: " . ($tiempos['recepcion_prom'] ?? '—') . "min, "; $ctx .= "servicio: " . ($tiempos['servicio_prom'] ?? '—') . "min, "; $ctx .= "total: " . ($tiempos['total_prom'] ?? '—') . "min.\n"; // Tiempos por recepcionista $rRecep = $pdo->prepare( "SELECT ur.full_name AS recepcionista, COUNT(*) AS atendidos, ROUND(AVG(TIMESTAMPDIFF(SECOND, t.inicio_recepcion_at, t.fin_recepcion_at) / 60), 1) AS prom_min FROM turnero_turnos t JOIN admin_users ur ON ur.id = t.atendido_recepcion_por WHERE t.sesion_id = ? AND t.fin_recepcion_at IS NOT NULL GROUP BY ur.id, ur.full_name ORDER BY prom_min DESC" ); $rRecep->execute([$sesId]); $recepStats = $rRecep->fetchAll(PDO::FETCH_ASSOC); if ($recepStats) { $ctx .= "\nTiempo promedio por recepcionista:\n"; foreach ($recepStats as $r) { $ctx .= "- {$r['recepcionista']}: {$r['prom_min']} min/paciente ({$r['atendidos']} atendidos)\n"; } } // Facturación del día $rFact = $pdo->prepare( "SELECT SUM(ts.total_cobrado) AS total, COUNT(*) AS n, ts.metodo_pago FROM turnero_solicitudes ts JOIN turnero_turnos t ON t.id = ts.turno_id WHERE t.sesion_id = ? AND ts.total_cobrado > 0 GROUP BY ts.metodo_pago" ); $rFact->execute([$sesId]); $factRows = $rFact->fetchAll(PDO::FETCH_ASSOC); if ($factRows) { $ctx .= "\nFacturación del día:\n"; $totalFact = 0; foreach ($factRows as $f) { $ctx .= "- {$f['metodo_pago']}: $" . number_format((float)$f['total'], 0, ',', '.') . " ({$f['n']} atenciones)\n"; $totalFact += (float)$f['total']; } $ctx .= "Total cobrado hoy: $" . number_format($totalFact, 0, ',', '.') . "\n"; } // Franjas horarias (cada 2h) $rFranjas = $pdo->prepare( "SELECT FLOOR(HOUR(creado_at) / 2) * 2 AS franja, COUNT(*) AS n FROM turnero_turnos WHERE sesion_id = ? GROUP BY franja ORDER BY franja" ); $rFranjas->execute([$sesId]); $franjas = $rFranjas->fetchAll(PDO::FETCH_ASSOC); if ($franjas) { $ctx .= "\nTurnos por franja horaria:\n"; foreach ($franjas as $f) { $h = (int)$f['franja']; $ctx .= "- {$h}:00–" . ($h+2) . ":00 → {$f['n']} turnos\n"; } } // Detalle completo de turnos (máx 60) $tList = $pdo->prepare( "SELECT t.codigo, t.estado, p.codigo AS prioridad, COALESCE(sp.nombre_completo, t.paciente_nombre) AS paciente, sp.numero_documento AS doc, l.nombre AS lugar, ur.full_name AS recepcionista, ul.full_name AS bacteriologo, ts.total_cobrado AS cobrado, ts.metodo_pago, ROUND(TIMESTAMPDIFF(SECOND, t.inicio_recepcion_at, t.fin_recepcion_at) / 60, 1) AS min_recepcion, ROUND(TIMESTAMPDIFF(SECOND, t.inicio_lugar_at, t.fin_lugar_at) / 60, 1) AS min_servicio, ROUND(TIMESTAMPDIFF(SECOND, t.creado_at, COALESCE(t.fin_lugar_at, NOW())) / 60, 1) AS min_total, GROUP_CONCAT(DISTINCT et.nombre ORDER BY et.nombre SEPARATOR ', ') AS examenes FROM turnero_turnos t JOIN turnero_prioridades p ON p.id = t.prioridad_id LEFT JOIN turnero_lugares l ON l.id = t.lugar_destino_id LEFT JOIN turnero_solicitudes ts ON ts.turno_id = t.id LEFT JOIN turnero_examen_items tei ON tei.solicitud_id = ts.id LEFT JOIN exam_tipos et ON et.id = tei.exam_tipo_id LEFT JOIN lab_pacientes sp ON sp.id = ts.paciente_id LEFT JOIN admin_users ur ON ur.id = t.atendido_recepcion_por LEFT JOIN admin_users ul ON ul.id = t.atendido_lugar_por WHERE t.sesion_id = ? GROUP BY t.id ORDER BY t.numero ASC LIMIT 60" ); $tList->execute([$sesId]); $turnos = $tList->fetchAll(PDO::FETCH_ASSOC); if ($turnos) { $ctx .= "\nDetalle de cada turno:\n"; foreach ($turnos as $t) { $ctx .= "- {$t['codigo']} [{$t['prioridad']}] {$t['paciente']}"; if (!empty($t['doc'])) $ctx .= " (CC: {$t['doc']})"; if (!empty($t['lugar'])) $ctx .= " → {$t['lugar']}"; if (!empty($t['recepcionista'])) $ctx .= " | Recep: {$t['recepcionista']}"; if (!empty($t['bacteriologo'])) $ctx .= " | Bact: {$t['bacteriologo']}"; if (!empty($t['examenes'])) $ctx .= " | Exámenes: {$t['examenes']}"; if ($t['min_recepcion'] !== null) $ctx .= " | T.recep: {$t['min_recepcion']}min"; if ($t['min_servicio'] !== null) $ctx .= " | T.serv: {$t['min_servicio']}min"; if ($t['min_total'] !== null) $ctx .= " | T.total: {$t['min_total']}min"; if (!empty($t['cobrado'])) $ctx .= " | $" . number_format((float)$t['cobrado'], 0, ',', '.'); if (!empty($t['metodo_pago'])) $ctx .= " ({$t['metodo_pago']})"; $ctx .= " [{$t['estado']}]\n"; } } } // ── Prompt ──────────────────────────────────────────────────── $systemPrompt = << ['parts' => [['text' => $systemPrompt]]], 'contents' => [ ['role' => 'user', 'parts' => [['text' => $pregunta]]] ], 'generationConfig' => [ 'temperature' => 0.3, 'maxOutputTokens' => 600, ], ], JSON_UNESCAPED_UNICODE); $ch = curl_init("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={$apiKey}"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, ]); $raw = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if (!$raw) jsonError('Sin respuesta del servicio de IA', 502); $gemini = json_decode($raw, true); if ($code !== 200 || empty($gemini['candidates'][0]['content']['parts'][0]['text'])) { $detail = $gemini['error']['message'] ?? 'Error desconocido'; jsonError("Error IA: {$detail}", 502); } $respuesta = $gemini['candidates'][0]['content']['parts'][0]['text']; jsonOk(['respuesta' => $respuesta]);