Files
donna/public/calendario/api.php
T
Soporte ZH Consultores d26ab5e855 feat(calendario): eventos manuales — reservas, cumpleaños, recordatorios
- Nueva tabla calendar_events (tipo, fecha, hora_inicio/fin, ubicación,
  descripción, creador) — migración 011
- API CRUD /calendario/api.php (list/create/update/delete) con AuditLog
- Permisos: cualquiera autenticado crea; solo creador o Socio/Admin
  edita y elimina
- UI calendario: chip "Eventos", botón "+ Nuevo evento" en toolbar y
  en vista Día, modal con tipo/fecha/horario/ubicación, opción
  "Todo el día"
- Detalle de evento manual muestra creador, horario y ubicación;
  acciones Editar/Eliminar inline (sin navegación) si tiene permiso
- Colores por tipo: reserva sala, cumpleaños, recordatorio, evento
2026-06-04 20:49:23 -05:00

169 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/../../src/Auth.php';
require_once __DIR__ . '/../../src/Db.php';
require_once __DIR__ . '/../../src/AuditLog.php';
Auth::requireAuth();
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
const TIPOS = ['reserva_sala','cumpleanos','recordatorio','evento'];
function ok($d = null): void { echo json_encode(['ok'=>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);
}