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',
};
}