true, 'data'=>$d, 'user'=>Auth::user()], JSON_UNESCAPED_UNICODE); exit; } function err(string $m, int $c = 400): void { http_response_code($c); echo json_encode(['ok'=>false, 'error'=>$m], JSON_UNESCAPED_UNICODE); exit; } function body(): array { return json_decode((string) file_get_contents('php://input'), true) ?? []; } function clean_date(?string $v): ?string { if (!$v) return null; return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : null; } function clean_time(?string $v): ?string { if (!$v) return null; return preg_match('/^\d{2}:\d{2}(:\d{2})?$/', $v) ? substr($v, 0, 5) : null; } $method = $_SERVER['REQUEST_METHOD']; $action = $_GET['action'] ?? ''; $user = Auth::user(); $pdo = Db::pdo(); try { if ($method === 'GET' && $action === 'list') { $from = clean_date($_GET['from'] ?? null); $to = clean_date($_GET['to'] ?? null); $where = ['1=1']; $params = []; if ($from) { $where[] = 'fecha >= :from'; $params[':from'] = $from; } if ($to) { $where[] = 'fecha <= :to'; $params[':to'] = $to; } $sql = 'SELECT * FROM calendar_events WHERE ' . implode(' AND ', $where) . ' ORDER BY fecha ASC, hora_inicio ASC'; $stmt = $pdo->prepare($sql); $stmt->execute($params); $rows = $stmt->fetchAll(); foreach ($rows as &$r) { $r['id'] = (int)$r['id']; } ok($rows); } if ($method === 'POST' && $action === 'create') { $b = body(); $titulo = trim((string)($b['titulo'] ?? '')); $descripcion = trim((string)($b['descripcion'] ?? '')); $tipo = in_array($b['tipo'] ?? '', TIPOS, true) ? $b['tipo'] : 'evento'; $fecha = clean_date($b['fecha'] ?? null); $horaInicio = clean_time($b['hora_inicio'] ?? null); $horaFin = clean_time($b['hora_fin'] ?? null); $ubicacion = trim((string)($b['ubicacion'] ?? '')); if ($titulo === '') err('Título requerido'); if (!$fecha) err('Fecha requerida'); if ($horaFin && !$horaInicio) err('Si hay hora fin debe haber hora inicio'); if ($horaInicio && $horaFin && strcmp($horaFin, $horaInicio) < 0) { err('Hora fin debe ser >= hora inicio'); } $stmt = $pdo->prepare( 'INSERT INTO calendar_events (titulo, descripcion, tipo, fecha, hora_inicio, hora_fin, ubicacion, creado_por, creado_por_nombre) VALUES (:titulo, :desc, :tipo, :fecha, :hi, :hf, :ub, :cp, :cpn)' ); $stmt->execute([ ':titulo' => $titulo, ':desc' => $descripcion ?: null, ':tipo' => $tipo, ':fecha' => $fecha, ':hi' => $horaInicio, ':hf' => $horaFin, ':ub' => $ubicacion ?: null, ':cp' => $user['email'], ':cpn' => $user['name'] ?: $user['email'], ]); $id = (int)$pdo->lastInsertId(); AuditLog::log('calendario', 'create', 'evento', (string)$id, $titulo, ['tipo'=>$tipo, 'fecha'=>$fecha]); $sel = $pdo->prepare('SELECT * FROM calendar_events WHERE id = :id'); $sel->execute([':id' => $id]); $row = $sel->fetch(); $row['id'] = (int)$row['id']; ok($row); } if ($method === 'PUT' && $action === 'update') { $b = body(); $id = (int)($b['_id'] ?? 0); if (!$id) err('id requerido'); $sel = $pdo->prepare('SELECT * FROM calendar_events WHERE id = :id'); $sel->execute([':id' => $id]); $before = $sel->fetch(); if (!$before) err('Evento no encontrado', 404); if (!Auth::isSocio() && $before['creado_por'] !== $user['email']) { err('Solo el creador (o un Socio/Admin) puede editar este evento', 403); } $titulo = trim((string)($b['titulo'] ?? $before['titulo'])); $descripcion = isset($b['descripcion']) ? trim((string)$b['descripcion']) : ($before['descripcion'] ?? ''); $tipo = in_array($b['tipo'] ?? $before['tipo'], TIPOS, true) ? ($b['tipo'] ?? $before['tipo']) : $before['tipo']; $fecha = clean_date($b['fecha'] ?? null) ?: $before['fecha']; $horaInicio = array_key_exists('hora_inicio', $b) ? clean_time($b['hora_inicio']) : $before['hora_inicio']; $horaFin = array_key_exists('hora_fin', $b) ? clean_time($b['hora_fin']) : $before['hora_fin']; $ubicacion = isset($b['ubicacion']) ? trim((string)$b['ubicacion']) : ($before['ubicacion'] ?? ''); if ($titulo === '') err('Título requerido'); if ($horaFin && !$horaInicio) err('Si hay hora fin debe haber hora inicio'); if ($horaInicio && $horaFin && strcmp($horaFin, $horaInicio) < 0) { err('Hora fin debe ser >= hora inicio'); } $stmt = $pdo->prepare( 'UPDATE calendar_events SET titulo = :titulo, descripcion = :desc, tipo = :tipo, fecha = :fecha, hora_inicio = :hi, hora_fin = :hf, ubicacion = :ub WHERE id = :id' ); $stmt->execute([ ':titulo' => $titulo, ':desc' => $descripcion ?: null, ':tipo' => $tipo, ':fecha' => $fecha, ':hi' => $horaInicio, ':hf' => $horaFin, ':ub' => $ubicacion ?: null, ':id' => $id, ]); AuditLog::log('calendario', 'update', 'evento', (string)$id, $titulo); $sel->execute([':id' => $id]); $row = $sel->fetch(); $row['id'] = (int)$row['id']; ok($row); } if ($method === 'DELETE' && $action === 'delete') { $id = (int)($_GET['id'] ?? 0); if (!$id) err('id requerido'); $sel = $pdo->prepare('SELECT id, titulo, creado_por FROM calendar_events WHERE id = :id'); $sel->execute([':id' => $id]); $before = $sel->fetch(); if (!$before) err('Evento no encontrado', 404); if (!Auth::isSocio() && $before['creado_por'] !== $user['email']) { err('Solo el creador (o un Socio/Admin) puede eliminar este evento', 403); } $pdo->prepare('DELETE FROM calendar_events WHERE id = :id')->execute([':id' => $id]); AuditLog::log('calendario', 'delete', 'evento', (string)$id, $before['titulo']); ok(['deleted' => $id]); } err('Acción no reconocida', 404); } catch (PDOException $e) { error_log('calendario db error: ' . $e->getMessage()); err('Error de base de datos', 500); } catch (Throwable $e) { error_log('calendario error: ' . $e->getMessage()); err('Error interno', 500); }