false, 'error' => 'Error interno: ' . $err['message']]); } }); header('Content-Type: application/json; charset=utf-8'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type'); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; } // ── Respuestas JSON ────────────────────────────────────────── function jsonOk(array $payload = [], string $mensaje = ''): void { ob_clean(); $resp = ['ok' => true, 'success' => true]; if ($mensaje) { $resp['message'] = $mensaje; } echo json_encode(array_merge($resp, $payload), JSON_UNESCAPED_UNICODE); exit; } function jsonError(string $mensaje, int $code = 400): void { ob_clean(); http_response_code($code); echo json_encode(['ok' => false, 'success' => false, 'error' => $mensaje], JSON_UNESCAPED_UNICODE); exit; } function requireMethod(string $method): void { if ($_SERVER['REQUEST_METHOD'] !== strtoupper($method)) { jsonError('Método no permitido', 405); } } function inputJson(): array { static $parsed = null; if ($parsed === null) { $raw = file_get_contents('php://input'); $parsed = json_decode($raw, true) ?? []; } return $parsed; } // ── Autenticación ──────────────────────────────────────────── function requireAuth(): void { if (empty($_SESSION['admin_user']['id'])) { jsonError('No autenticado', 401); } } /** * Exige sesión + acceso al módulo registro_exams. */ function requireExams(): void { requireAuth(); $modules = $_SESSION['admin_user']['modules'] ?? []; $roleId = $_SESSION['admin_user']['role_id'] ?? null; // Superadmin / admin global = acceso total if (empty($roleId)) { return; } if (!in_array('registro_exams', (array) $modules, true)) { jsonError('Sin acceso al módulo Registro Exámenes', 403); } } function adminId(): ?int { return isset($_SESSION['admin_user']['id']) ? (int) $_SESSION['admin_user']['id'] : null; } // ── Helper BD ──────────────────────────────────────────────── function db(): PDO { return Database::getInstance()->getConnection(); } // ── Código de orden ────────────────────────────────────────── /** * Genera el siguiente código de orden legible: EX-YYYYMMDD-NNNN * Usa SELECT FOR UPDATE para evitar duplicados en concurrencia. */ function generarCodigoOrden(): string { $pdo = db(); $hoy = date('Ymd'); $prefijo = "EX-{$hoy}-"; $stmt = $pdo->prepare( "SELECT COALESCE(MAX(CAST(SUBSTRING(codigo, -4) AS UNSIGNED)), 0) + 1 AS siguiente FROM exam_ordenes WHERE codigo LIKE ? FOR UPDATE" ); $stmt->execute(["{$prefijo}%"]); $n = (int) $stmt->fetchColumn(); return $prefijo . str_pad($n, 4, '0', STR_PAD_LEFT); } // ── Etiqueta de prioridad ──────────────────────────────────── function badgePrioridad(string $p): string { return match ($p) { 'urgente' => 'Urgente', 'stat' => 'STAT', default => 'Normal', }; } function badgeEstadoOrden(string $e): string { return match ($e) { 'en_proceso' => 'En proceso', 'completa' => 'Completa', 'entregada' => 'Entregada', 'cancelada' => 'Cancelada', default => 'Pendiente', }; }