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
This commit is contained in:
@@ -0,0 +1,168 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Donna — Schema 011: calendar_events (entradas manuales del calendario)
|
||||||
|
-- ============================================================
|
||||||
|
-- Reservas de sala, cumpleaños, recordatorios y eventos sueltos que
|
||||||
|
-- no son trámites ni escrituras.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS calendar_events (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
titulo VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
descripcion TEXT NULL,
|
||||||
|
tipo ENUM('reserva_sala','cumpleanos','recordatorio','evento')
|
||||||
|
NOT NULL DEFAULT 'evento',
|
||||||
|
fecha DATE NOT NULL,
|
||||||
|
hora_inicio TIME NULL,
|
||||||
|
hora_fin TIME NULL,
|
||||||
|
ubicacion VARCHAR(255) NULL,
|
||||||
|
creado_por VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
creado_por_nombre VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_fecha (fecha),
|
||||||
|
INDEX idx_creado_por (creado_por),
|
||||||
|
INDEX idx_tipo (tipo)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
+327
-28
@@ -90,16 +90,47 @@ header { background:var(--navy); border-bottom:3px solid var(--gold); padding:0
|
|||||||
.chip.on { background:#fff; border-color:currentColor; }
|
.chip.on { background:#fff; border-color:currentColor; }
|
||||||
.chip.off { background:var(--gray-100); color:var(--gray-400) !important; opacity:.65; }
|
.chip.off { background:var(--gray-100); color:var(--gray-400) !important; opacity:.65; }
|
||||||
.chip.off .cdot { background:var(--gray-400) !important; }
|
.chip.off .cdot { background:var(--gray-400) !important; }
|
||||||
.chip[data-src="esc"] { color:var(--c-esc); }
|
.chip[data-src="esc"] { color:var(--c-esc); }
|
||||||
.chip[data-src="adm"] { color:var(--c-adm); }
|
.chip[data-src="adm"] { color:var(--c-adm); }
|
||||||
.chip[data-src="corp"] { color:var(--c-corp); }
|
.chip[data-src="corp"] { color:var(--c-corp); }
|
||||||
.chip[data-src="mig"] { color:var(--c-mig); }
|
.chip[data-src="mig"] { color:var(--c-mig); }
|
||||||
.chip[data-src="lit"] { color:var(--c-lit); }
|
.chip[data-src="lit"] { color:var(--c-lit); }
|
||||||
.chip[data-src="esc"] .cdot { background:var(--c-esc); }
|
.chip[data-src="manual"] { color:#27ae60; }
|
||||||
.chip[data-src="adm"] .cdot { background:var(--c-adm); }
|
.chip[data-src="esc"] .cdot { background:var(--c-esc); }
|
||||||
.chip[data-src="corp"] .cdot { background:var(--c-corp); }
|
.chip[data-src="adm"] .cdot { background:var(--c-adm); }
|
||||||
.chip[data-src="mig"] .cdot { background:var(--c-mig); }
|
.chip[data-src="corp"] .cdot { background:var(--c-corp); }
|
||||||
.chip[data-src="lit"] .cdot { background:var(--c-lit); }
|
.chip[data-src="mig"] .cdot { background:var(--c-mig); }
|
||||||
|
.chip[data-src="lit"] .cdot { background:var(--c-lit); }
|
||||||
|
.chip[data-src="manual"] .cdot { background:#27ae60; }
|
||||||
|
|
||||||
|
/* Botón Nuevo evento manual */
|
||||||
|
.btn-new-event { padding:7px 12px; background:#27ae60; color:#fff; border:none; border-radius:8px;
|
||||||
|
font-size:12.5px; font-weight:500; cursor:pointer; display:inline-flex; align-items:center; gap:5px;
|
||||||
|
transition:background .15s; }
|
||||||
|
.btn-new-event:hover { background:#1d8049; }
|
||||||
|
.btn-new-event svg { width:14px; height:14px; }
|
||||||
|
|
||||||
|
/* Modal de evento manual */
|
||||||
|
.evt-form .fg { margin-bottom:14px; }
|
||||||
|
.evt-form .fg label { display:block; font-size:10.5px; font-weight:600; color:var(--navy);
|
||||||
|
margin-bottom:5px; text-transform:uppercase; letter-spacing:.08em; }
|
||||||
|
.evt-form .fg input, .evt-form .fg select, .evt-form .fg textarea {
|
||||||
|
width:100%; padding:8px 11px; border:1px solid var(--gray-200); border-radius:8px;
|
||||||
|
font-family:'DM Sans',sans-serif; font-size:13px; background:#fff;
|
||||||
|
}
|
||||||
|
.evt-form .fg textarea { resize:vertical; min-height:60px; }
|
||||||
|
.evt-form .fg input:focus, .evt-form .fg select:focus, .evt-form .fg textarea:focus {
|
||||||
|
outline:none; border-color:var(--navy-l); box-shadow:0 0 0 3px rgba(27,58,107,.10);
|
||||||
|
}
|
||||||
|
.evt-form .fg-row { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
||||||
|
.evt-form .fg-check { display:flex; align-items:center; gap:8px; font-size:13px; color:var(--gray-700); cursor:pointer; }
|
||||||
|
.evt-form .fg-check input { width:auto; margin:0; }
|
||||||
|
.btn-cancel { padding:8px 16px; background:transparent; color:var(--gray-700); border:1px solid var(--gray-200); border-radius:8px; font-size:13px; }
|
||||||
|
.btn-cancel:hover { background:var(--gray-100); }
|
||||||
|
.btn-save { padding:8px 20px; background:var(--navy); color:#fff; border:none; border-radius:8px; font-size:13px; font-weight:500; }
|
||||||
|
.btn-save:hover { background:var(--navy-d); }
|
||||||
|
.btn-delete-evt { padding:8px 14px; background:#fff; border:1px solid #e74c3c; color:#c0392b; border-radius:8px; font-size:13px; }
|
||||||
|
.btn-delete-evt:hover { background:#c0392b; color:#fff; }
|
||||||
|
|
||||||
.cal-kpis { display:grid; grid-template-columns:repeat(4,1fr); gap:10px; padding:14px 24px 0; }
|
.cal-kpis { display:grid; grid-template-columns:repeat(4,1fr); gap:10px; padding:14px 24px 0; }
|
||||||
.cal-kpi { background:#fff; border:1px solid var(--gray-100); border-radius:10px; padding:12px 16px;
|
.cal-kpi { background:#fff; border:1px solid var(--gray-100); border-radius:10px; padding:12px 16px;
|
||||||
@@ -311,7 +342,12 @@ footer.legend .pill { display:inline-block; padding:2px 8px; border-radius:20px;
|
|||||||
<span class="chip on" data-src="corp"><span class="cdot"></span>Corporativo</span>
|
<span class="chip on" data-src="corp"><span class="cdot"></span>Corporativo</span>
|
||||||
<span class="chip on" data-src="mig"><span class="cdot"></span>Migración</span>
|
<span class="chip on" data-src="mig"><span class="cdot"></span>Migración</span>
|
||||||
<span class="chip on" data-src="lit"><span class="cdot"></span>Litigios</span>
|
<span class="chip on" data-src="lit"><span class="cdot"></span>Litigios</span>
|
||||||
|
<span class="chip on" data-src="manual"><span class="cdot"></span>Eventos</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn-new-event" onclick="openNewEvent()" title="Reservar sala, agregar cumpleaños o recordatorio">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
|
||||||
|
Nuevo evento
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cal-kpis">
|
<div class="cal-kpis">
|
||||||
@@ -346,19 +382,82 @@ footer.legend .pill { display:inline-block; padding:2px 8px; border-radius:20px;
|
|||||||
<button class="close" onclick="closeEvent()" title="Cerrar (Esc)">✕</button>
|
<button class="close" onclick="closeEvent()" title="Cerrar (Esc)">✕</button>
|
||||||
</header>
|
</header>
|
||||||
<div class="modal-body" id="evt-body"></div>
|
<div class="modal-body" id="evt-body"></div>
|
||||||
<div class="modal-foot">
|
<div class="modal-foot" id="evt-foot">
|
||||||
<button class="btn-secondary" onclick="closeEvent()">← Regresar al calendario</button>
|
<button class="btn-secondary" onclick="closeEvent()">← Regresar al calendario</button>
|
||||||
<button class="btn-primary" id="btn-edit">Editar evento</button>
|
<button class="btn-primary" id="btn-edit">Editar evento</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: crear/editar evento manual del calendario -->
|
||||||
|
<div class="modal-overlay" id="ev-form-modal">
|
||||||
|
<div class="modal">
|
||||||
|
<header class="modal-head">
|
||||||
|
<h3 id="ev-form-title">Nuevo evento</h3>
|
||||||
|
<button class="close" onclick="closeEventForm()" title="Cerrar (Esc)">✕</button>
|
||||||
|
</header>
|
||||||
|
<div class="modal-body evt-form">
|
||||||
|
<div class="fg">
|
||||||
|
<label>Título *</label>
|
||||||
|
<input type="text" id="ev-titulo" placeholder="Ej: Reunión con cliente, Cumpleaños de Mario, Sala de conferencias">
|
||||||
|
</div>
|
||||||
|
<div class="fg-row">
|
||||||
|
<div class="fg">
|
||||||
|
<label>Tipo</label>
|
||||||
|
<select id="ev-tipo">
|
||||||
|
<option value="evento">Evento</option>
|
||||||
|
<option value="reserva_sala">Reserva de sala</option>
|
||||||
|
<option value="cumpleanos">Cumpleaños</option>
|
||||||
|
<option value="recordatorio">Recordatorio</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Fecha *</label>
|
||||||
|
<input type="date" id="ev-fecha">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label class="fg-check"><input type="checkbox" id="ev-allday" onchange="toggleAllDay()"> Todo el día</label>
|
||||||
|
</div>
|
||||||
|
<div class="fg-row" id="ev-times">
|
||||||
|
<div class="fg">
|
||||||
|
<label>Hora inicio</label>
|
||||||
|
<input type="time" id="ev-hi">
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Hora fin (opcional)</label>
|
||||||
|
<input type="time" id="ev-hf">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Ubicación</label>
|
||||||
|
<input type="text" id="ev-ub" placeholder="Sala 1, oficina, etc.">
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Descripción</label>
|
||||||
|
<textarea id="ev-desc" placeholder="Notas adicionales…"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot" style="justify-content:space-between;">
|
||||||
|
<button class="btn-delete-evt" id="ev-delete-btn" onclick="deleteCurrentEvent()" style="display:none;">Eliminar evento</button>
|
||||||
|
<div style="display:flex;gap:10px;margin-left:auto;">
|
||||||
|
<button class="btn-cancel" onclick="closeEventForm()">Cancelar</button>
|
||||||
|
<button class="btn-save" id="ev-save-btn" onclick="saveEventForm()">Guardar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const DEPT_LABELS = {adm:'Administrativo', corp:'Corporativo', mig:'Migración', lit:'Litigios'};
|
const DEPT_LABELS = {adm:'Administrativo', corp:'Corporativo', mig:'Migración', lit:'Litigios'};
|
||||||
const SRC_LABELS = {esc:'Escrituras', adm:'Trámites Admin', corp:'Trámites Corp', mig:'Trámites Mig', lit:'Trámites Lit'};
|
const SRC_LABELS = {esc:'Escrituras', adm:'Trámites Admin', corp:'Trámites Corp', mig:'Trámites Mig', lit:'Trámites Lit', manual:'Evento'};
|
||||||
const SRC_COLORS = { esc:'#1B3A6B', adm:'#6c5b95', corp:'#9E7C2E', mig:'#2980b9', lit:'#722f37' };
|
const SRC_COLORS = { esc:'#1B3A6B', adm:'#6c5b95', corp:'#9E7C2E', mig:'#2980b9', lit:'#722f37', manual:'#27ae60' };
|
||||||
|
const MANUAL_TIPO_COLORS = { reserva_sala:'#2e7d8f', cumpleanos:'#c2185b', recordatorio:'#e67e22', evento:'#27ae60' };
|
||||||
|
const MANUAL_TIPO_LABELS = { reserva_sala:'Reserva de sala', cumpleanos:'Cumpleaños', recordatorio:'Recordatorio', evento:'Evento' };
|
||||||
|
const MY_EMAIL_CAL = (window.DONNA && window.DONNA.me) || '';
|
||||||
|
const IS_SOCIO_CAL = !!(window.DONNA && window.DONNA.isSocio);
|
||||||
const COLOR_OVERDUE = '#c0392b';
|
const COLOR_OVERDUE = '#c0392b';
|
||||||
const COLOR_UPCOMING = '#e67e22';
|
const COLOR_UPCOMING = '#e67e22';
|
||||||
const MES_ES = ['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre'];
|
const MES_ES = ['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre'];
|
||||||
@@ -369,7 +468,8 @@ const CLOSED_STATUS = new Set(['Culminado','Facturado','Inscrito']);
|
|||||||
let events = [];
|
let events = [];
|
||||||
let view = 'month';
|
let view = 'month';
|
||||||
let cursor = stripTime(new Date());
|
let cursor = stripTime(new Date());
|
||||||
let filters = new Set(['esc','adm','corp','mig','lit']);
|
let filters = new Set(['esc','adm','corp','mig','lit','manual']);
|
||||||
|
let editingEvId = null; // id del evento manual siendo editado (null = nuevo)
|
||||||
let search = '';
|
let search = '';
|
||||||
let selected = null;
|
let selected = null;
|
||||||
let authUser = { email:'', name:'' };
|
let authUser = { email:'', name:'' };
|
||||||
@@ -433,8 +533,15 @@ function setUserChip(){
|
|||||||
if($('user-name')) { $('user-name').textContent=name||email||'Conectado'; $('user-name').title=email; }
|
if($('user-name')) { $('user-name').textContent=name||email||'Conectado'; $('user-name').title=email; }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function apiCall(url){
|
async function apiCall(url, opts){
|
||||||
const res = await fetch(url, {headers:{Accept:'application/json'}, credentials:'same-origin'});
|
const method = (opts && opts.method) || 'GET';
|
||||||
|
const headers = {Accept:'application/json'};
|
||||||
|
const init = {method, headers, credentials:'same-origin'};
|
||||||
|
if (opts && opts.body) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
init.body = opts.body;
|
||||||
|
}
|
||||||
|
const res = await fetch(url, init);
|
||||||
let j;
|
let j;
|
||||||
try { j = await res.json(); } catch(e){ throw new Error('JSON inválido'); }
|
try { j = await res.json(); } catch(e){ throw new Error('JSON inválido'); }
|
||||||
if(res.status===401 && j.auth_required){
|
if(res.status===401 && j.auth_required){
|
||||||
@@ -450,7 +557,7 @@ function titleOf(t, dept){
|
|||||||
return t.nombre || t.tipo_tramite || t.tipo_proceso || (DEPT_LABELS[dept]||'Trámite');
|
return t.nombre || t.tipo_tramite || t.tipo_proceso || (DEPT_LABELS[dept]||'Trámite');
|
||||||
}
|
}
|
||||||
|
|
||||||
function flatten(escrituras, tramites){
|
function flatten(escrituras, tramites, manualEvents){
|
||||||
const out = [];
|
const out = [];
|
||||||
for(const e of (escrituras||[])){
|
for(const e of (escrituras||[])){
|
||||||
if(e.fecha_solicitud){
|
if(e.fecha_solicitud){
|
||||||
@@ -496,6 +603,28 @@ function flatten(escrituras, tramites){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Eventos manuales (calendar_events)
|
||||||
|
for(const m of (manualEvents||[])){
|
||||||
|
const dateStr = ymd(m.fecha);
|
||||||
|
if (!dateStr) continue;
|
||||||
|
let timeRange = '';
|
||||||
|
if (m.hora_inicio) {
|
||||||
|
timeRange = m.hora_inicio.substring(0,5);
|
||||||
|
if (m.hora_fin) timeRange += '–' + m.hora_fin.substring(0,5);
|
||||||
|
}
|
||||||
|
const title = (timeRange ? timeRange + ' · ' : '') + (m.titulo || 'Sin título');
|
||||||
|
out.push({
|
||||||
|
key:'man-'+m.id, source:'manual', type:m.tipo || 'evento',
|
||||||
|
date: dateStr, title: title,
|
||||||
|
cliente: m.ubicacion || '',
|
||||||
|
abogado: m.creado_por_nombre || m.creado_por || '',
|
||||||
|
estatus: '', raw:m, ref_id:m.id,
|
||||||
|
nav: null, // manual events use inline edit, no navigation
|
||||||
|
manual_id: m.id, manual_titulo: m.titulo, manual_tipo: m.tipo,
|
||||||
|
manual_hi: m.hora_inicio, manual_hf: m.hora_fin,
|
||||||
|
manual_creado_por: m.creado_por,
|
||||||
|
});
|
||||||
|
}
|
||||||
return out
|
return out
|
||||||
.filter(e => e.date)
|
.filter(e => e.date)
|
||||||
.map(e => Object.assign(e, { daysOut: daysToToday(e.date) }))
|
.map(e => Object.assign(e, { daysOut: daysToToday(e.date) }))
|
||||||
@@ -505,12 +634,13 @@ function flatten(escrituras, tramites){
|
|||||||
async function loadAll(){
|
async function loadAll(){
|
||||||
setSync('syncing','Cargando…');
|
setSync('syncing','Cargando…');
|
||||||
try {
|
try {
|
||||||
const [escData, traData] = await Promise.all([
|
const [escData, traData, manData] = await Promise.all([
|
||||||
apiCall('/escrituras/api.php?action=list'),
|
apiCall('/escrituras/api.php?action=list'),
|
||||||
apiCall('/tramites/api.php?action=list'),
|
apiCall('/tramites/api.php?action=list'),
|
||||||
|
apiCall('/calendario/api.php?action=list'),
|
||||||
]);
|
]);
|
||||||
if(escData===null || traData===null) return;
|
if(escData===null || traData===null || manData===null) return;
|
||||||
events = flatten(escData, traData);
|
events = flatten(escData, traData, manData);
|
||||||
setUserChip();
|
setUserChip();
|
||||||
setSync('ok', events.length + ' eventos cargados');
|
setSync('ok', events.length + ' eventos cargados');
|
||||||
render();
|
render();
|
||||||
@@ -544,6 +674,7 @@ function colorFor(e){
|
|||||||
if(e.daysOut<0) return COLOR_OVERDUE;
|
if(e.daysOut<0) return COLOR_OVERDUE;
|
||||||
if(e.daysOut<=7) return COLOR_UPCOMING;
|
if(e.daysOut<=7) return COLOR_UPCOMING;
|
||||||
}
|
}
|
||||||
|
if (e.source === 'manual') return MANUAL_TIPO_COLORS[e.type] || SRC_COLORS.manual;
|
||||||
return SRC_COLORS[e.source] || '#666';
|
return SRC_COLORS[e.source] || '#666';
|
||||||
}
|
}
|
||||||
function badgeFor(e){
|
function badgeFor(e){
|
||||||
@@ -725,8 +856,12 @@ function renderDay(){
|
|||||||
const evs = eventsOnDate(cursor);
|
const evs = eventsOnDate(cursor);
|
||||||
const isToday = sameDate(cursor, today);
|
const isToday = sameDate(cursor, today);
|
||||||
const heading = DOW_FULL[(cursor.getDay()+6)%7] + ' ' + cursor.getDate() + ' de ' + MES_ES[cursor.getMonth()];
|
const heading = DOW_FULL[(cursor.getDay()+6)%7] + ' ' + cursor.getDate() + ' de ' + MES_ES[cursor.getMonth()];
|
||||||
|
const dayYmd = ymd(cursor);
|
||||||
let html = `<div class="day-view">
|
let html = `<div class="day-view">
|
||||||
<div class="day-header">${heading}${isToday?' · Hoy':''}</div>
|
<div class="day-header" style="display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;">
|
||||||
|
<span>${heading}${isToday?' · Hoy':''}</span>
|
||||||
|
<button class="btn-new-event" onclick='openNewEvent("${dayYmd}")'>+ Nuevo evento</button>
|
||||||
|
</div>
|
||||||
<div class="day-sub">${evs.length} ${evs.length===1?'evento':'eventos'}</div>`;
|
<div class="day-sub">${evs.length} ${evs.length===1?'evento':'eventos'}</div>`;
|
||||||
if(!evs.length){
|
if(!evs.length){
|
||||||
html += `<div class="cal-empty">No hay eventos este día.</div>`;
|
html += `<div class="cal-empty">No hay eventos este día.</div>`;
|
||||||
@@ -734,12 +869,17 @@ function renderDay(){
|
|||||||
html += evs.map(e=>{
|
html += evs.map(e=>{
|
||||||
const b = badgeFor(e);
|
const b = badgeFor(e);
|
||||||
const tag = b ? `<span class="evt-tag" style="background:${b.bg};color:#fff">${esc(b.label)}</span>` : '';
|
const tag = b ? `<span class="evt-tag" style="background:${b.bg};color:#fff">${esc(b.label)}</span>` : '';
|
||||||
const srcTag = `<span class="evt-tag" style="background:${SRC_COLORS[e.source]||'#888'};color:#fff">${esc(SRC_LABELS[e.source]||'')}</span>`;
|
const srcBg = (e.source==='manual') ? (MANUAL_TIPO_COLORS[e.type] || SRC_COLORS.manual) : (SRC_COLORS[e.source]||'#888');
|
||||||
|
const srcLbl = (e.source==='manual') ? (MANUAL_TIPO_LABELS[e.type] || 'Evento') : (SRC_LABELS[e.source]||'');
|
||||||
|
const srcTag = `<span class="evt-tag" style="background:${srcBg};color:#fff">${esc(srcLbl)}</span>`;
|
||||||
|
const meta = (e.source==='manual')
|
||||||
|
? `${esc(e.abogado||'—')}${e.raw && e.raw.ubicacion ? ' · '+esc(e.raw.ubicacion) : ''}`
|
||||||
|
: `${esc(e.cliente||'—')} · ${esc(e.abogado||'—')} · ${esc(e.estatus||'')}`;
|
||||||
return `<div class="day-evt" onclick='openEvent("${e.key}")'>
|
return `<div class="day-evt" onclick='openEvent("${e.key}")'>
|
||||||
<div class="day-evt-bar" style="background:${colorFor(e)}"></div>
|
<div class="day-evt-bar" style="background:${colorFor(e)}"></div>
|
||||||
<div>
|
<div>
|
||||||
<div class="day-evt-title">${esc(e.title)}</div>
|
<div class="day-evt-title">${esc(e.title)}</div>
|
||||||
<div class="day-evt-meta">${esc(e.cliente||'—')} · ${esc(e.abogado||'—')} · ${esc(e.estatus||'')}</div>
|
<div class="day-evt-meta">${meta}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="day-evt-badges">${tag}${srcTag}</div>
|
<div class="day-evt-badges">${tag}${srcTag}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -753,23 +893,178 @@ function openEvent(key){
|
|||||||
const e = events.find(x => x.key===key);
|
const e = events.find(x => x.key===key);
|
||||||
if(!e){ toast('Evento no encontrado','error'); return; }
|
if(!e){ toast('Evento no encontrado','error'); return; }
|
||||||
selected = e;
|
selected = e;
|
||||||
$('evt-title').textContent = e.title;
|
$('evt-title').textContent = e.manual_titulo || e.title;
|
||||||
$('evt-body').innerHTML = renderEventBody(e);
|
$('evt-body').innerHTML = renderEventBody(e);
|
||||||
$('btn-edit').onclick = ()=> { window.location.href = e.nav; };
|
const foot = $('evt-foot');
|
||||||
|
if (e.source === 'manual') {
|
||||||
|
const canEdit = IS_SOCIO_CAL || (e.manual_creado_por && e.manual_creado_por === MY_EMAIL_CAL);
|
||||||
|
foot.innerHTML = canEdit
|
||||||
|
? `<button class="btn-delete-evt" onclick="deleteManualFromDetail()">Eliminar</button>
|
||||||
|
<div style="display:flex;gap:10px;margin-left:auto;">
|
||||||
|
<button class="btn-secondary" onclick="closeEvent()">Cerrar</button>
|
||||||
|
<button class="btn-primary" onclick="editManualFromDetail()">Editar evento</button>
|
||||||
|
</div>`
|
||||||
|
: `<button class="btn-secondary" onclick="closeEvent()" style="margin-left:auto;">Cerrar</button>`;
|
||||||
|
} else {
|
||||||
|
foot.innerHTML =
|
||||||
|
`<button class="btn-secondary" onclick="closeEvent()">← Regresar al calendario</button>
|
||||||
|
<button class="btn-primary" id="btn-edit" onclick="if(selected&&selected.nav)window.location.href=selected.nav;">Editar evento</button>`;
|
||||||
|
}
|
||||||
$('evt-modal').classList.add('open');
|
$('evt-modal').classList.add('open');
|
||||||
}
|
}
|
||||||
|
function editManualFromDetail(){
|
||||||
|
const e = selected; if (!e) return;
|
||||||
|
closeEvent();
|
||||||
|
openEditEvent(e.manual_id);
|
||||||
|
}
|
||||||
|
function deleteManualFromDetail(){
|
||||||
|
const e = selected; if (!e) return;
|
||||||
|
if (!confirm('¿Eliminar este evento del calendario?')) return;
|
||||||
|
const id = e.manual_id;
|
||||||
|
closeEvent();
|
||||||
|
doDeleteManual(id);
|
||||||
|
}
|
||||||
function closeEvent(){
|
function closeEvent(){
|
||||||
selected = null;
|
selected = null;
|
||||||
$('evt-modal').classList.remove('open');
|
$('evt-modal').classList.remove('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Eventos manuales: crear / editar / eliminar =====
|
||||||
|
function openNewEvent(presetDate){
|
||||||
|
editingEvId = null;
|
||||||
|
$('ev-form-title').textContent = 'Nuevo evento';
|
||||||
|
$('ev-titulo').value = '';
|
||||||
|
$('ev-tipo').value = 'evento';
|
||||||
|
$('ev-fecha').value = presetDate || ymd(new Date());
|
||||||
|
$('ev-allday').checked = false;
|
||||||
|
$('ev-hi').value = '';
|
||||||
|
$('ev-hf').value = '';
|
||||||
|
$('ev-ub').value = '';
|
||||||
|
$('ev-desc').value = '';
|
||||||
|
$('ev-delete-btn').style.display = 'none';
|
||||||
|
$('ev-save-btn').textContent = 'Crear evento';
|
||||||
|
toggleAllDay();
|
||||||
|
$('ev-form-modal').classList.add('open');
|
||||||
|
setTimeout(()=>$('ev-titulo').focus(), 50);
|
||||||
|
}
|
||||||
|
function openEditEvent(id){
|
||||||
|
const e = events.find(x => x.source==='manual' && x.manual_id===id);
|
||||||
|
if(!e){ toast('Evento no encontrado','error'); return; }
|
||||||
|
const r = e.raw || {};
|
||||||
|
editingEvId = id;
|
||||||
|
$('ev-form-title').textContent = 'Editar evento';
|
||||||
|
$('ev-titulo').value = r.titulo || '';
|
||||||
|
$('ev-tipo').value = r.tipo || 'evento';
|
||||||
|
$('ev-fecha').value = ymd(r.fecha);
|
||||||
|
const noHours = !r.hora_inicio && !r.hora_fin;
|
||||||
|
$('ev-allday').checked = noHours;
|
||||||
|
$('ev-hi').value = r.hora_inicio ? r.hora_inicio.substring(0,5) : '';
|
||||||
|
$('ev-hf').value = r.hora_fin ? r.hora_fin.substring(0,5) : '';
|
||||||
|
$('ev-ub').value = r.ubicacion || '';
|
||||||
|
$('ev-desc').value = r.descripcion || '';
|
||||||
|
const canDelete = IS_SOCIO_CAL || (r.creado_por && r.creado_por === MY_EMAIL_CAL);
|
||||||
|
$('ev-delete-btn').style.display = canDelete ? '' : 'none';
|
||||||
|
$('ev-save-btn').textContent = 'Guardar cambios';
|
||||||
|
toggleAllDay();
|
||||||
|
$('ev-form-modal').classList.add('open');
|
||||||
|
setTimeout(()=>$('ev-titulo').focus(), 50);
|
||||||
|
}
|
||||||
|
function closeEventForm(){
|
||||||
|
$('ev-form-modal').classList.remove('open');
|
||||||
|
editingEvId = null;
|
||||||
|
}
|
||||||
|
function toggleAllDay(){
|
||||||
|
const allDay = $('ev-allday').checked;
|
||||||
|
$('ev-times').style.display = allDay ? 'none' : '';
|
||||||
|
if (allDay) { $('ev-hi').value = ''; $('ev-hf').value = ''; }
|
||||||
|
}
|
||||||
|
async function saveEventForm(){
|
||||||
|
const titulo = $('ev-titulo').value.trim();
|
||||||
|
const fecha = $('ev-fecha').value;
|
||||||
|
if (!titulo){ toast('Título requerido','error'); $('ev-titulo').focus(); return; }
|
||||||
|
if (!fecha) { toast('Fecha requerida','error'); $('ev-fecha').focus(); return; }
|
||||||
|
const allDay = $('ev-allday').checked;
|
||||||
|
const hi = allDay ? '' : $('ev-hi').value;
|
||||||
|
const hf = allDay ? '' : $('ev-hf').value;
|
||||||
|
if (hf && !hi){ toast('Si hay hora fin, también debe haber hora inicio','error'); return; }
|
||||||
|
if (hi && hf && hf < hi){ toast('Hora fin debe ser >= hora inicio','error'); return; }
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
titulo, fecha,
|
||||||
|
tipo: $('ev-tipo').value,
|
||||||
|
hora_inicio: hi || null,
|
||||||
|
hora_fin: hf || null,
|
||||||
|
ubicacion: $('ev-ub').value.trim(),
|
||||||
|
descripcion: $('ev-desc').value.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let url, method;
|
||||||
|
if (editingEvId){
|
||||||
|
payload._id = editingEvId;
|
||||||
|
url = '/calendario/api.php?action=update';
|
||||||
|
method = 'PUT';
|
||||||
|
} else {
|
||||||
|
url = '/calendario/api.php?action=create';
|
||||||
|
method = 'POST';
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = $('ev-save-btn'); const prev = btn.textContent;
|
||||||
|
btn.disabled = true; btn.textContent = 'Guardando…';
|
||||||
|
try {
|
||||||
|
const r = await apiCall(url, { method, body: JSON.stringify(payload) });
|
||||||
|
if (r === null) return;
|
||||||
|
toast(editingEvId ? 'Evento actualizado' : 'Evento creado');
|
||||||
|
closeEventForm();
|
||||||
|
await loadAll();
|
||||||
|
} catch(e){
|
||||||
|
toast('Error al guardar: '+e.message,'error');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false; btn.textContent = prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function deleteCurrentEvent(){
|
||||||
|
if (!editingEvId) return;
|
||||||
|
if (!confirm('¿Eliminar este evento del calendario?')) return;
|
||||||
|
const id = editingEvId;
|
||||||
|
closeEventForm();
|
||||||
|
doDeleteManual(id);
|
||||||
|
}
|
||||||
|
async function doDeleteManual(id){
|
||||||
|
try {
|
||||||
|
const r = await apiCall('/calendario/api.php?action=delete&id='+id, { method:'DELETE' });
|
||||||
|
if (r === null) return;
|
||||||
|
toast('Evento eliminado');
|
||||||
|
await loadAll();
|
||||||
|
} catch(e){
|
||||||
|
toast('Error al eliminar: '+e.message,'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
function renderEventBody(e){
|
function renderEventBody(e){
|
||||||
const srcColor = SRC_COLORS[e.source];
|
const srcColor = (e.source==='manual') ? (MANUAL_TIPO_COLORS[e.type] || SRC_COLORS.manual) : SRC_COLORS[e.source];
|
||||||
const srcLabel = SRC_LABELS[e.source];
|
const srcLabel = (e.source==='manual') ? (MANUAL_TIPO_LABELS[e.type] || 'Evento') : SRC_LABELS[e.source];
|
||||||
const tEvt = ({solicitud:'Fecha de Solicitud', vencimiento:'Fecha de Vencimiento', termino:'Fecha de Término'})[e.type] || e.type;
|
const tEvt = ({solicitud:'Fecha de Solicitud', vencimiento:'Fecha de Vencimiento', termino:'Fecha de Término'})[e.type] || e.type;
|
||||||
const d = parseYMD(e.date);
|
const d = parseYMD(e.date);
|
||||||
const dStr = d ? d.toLocaleDateString('es-PA',{weekday:'long', day:'2-digit', month:'long', year:'numeric'}) : e.date;
|
const dStr = d ? d.toLocaleDateString('es-PA',{weekday:'long', day:'2-digit', month:'long', year:'numeric'}) : e.date;
|
||||||
const r = e.raw || {};
|
const r = e.raw || {};
|
||||||
|
|
||||||
|
if (e.source === 'manual') {
|
||||||
|
const rows = [];
|
||||||
|
rows.push(['Tipo', `<span class="src-tag" style="background:${srcColor}">${esc(srcLabel)}</span>`]);
|
||||||
|
rows.push(['Fecha', `<b style="text-transform:capitalize">${esc(dStr)}</b>`]);
|
||||||
|
let hr = '';
|
||||||
|
if (e.manual_hi) {
|
||||||
|
hr = e.manual_hi.substring(0,5);
|
||||||
|
if (e.manual_hf) hr += ' – ' + e.manual_hf.substring(0,5);
|
||||||
|
} else {
|
||||||
|
hr = '<span style="color:var(--gray-400)">Todo el día</span>';
|
||||||
|
}
|
||||||
|
rows.push(['Horario', hr]);
|
||||||
|
if (r.ubicacion) rows.push(['Ubicación', esc(r.ubicacion)]);
|
||||||
|
rows.push(['Creado por', esc(e.abogado || '—')]);
|
||||||
|
if (r.descripcion) rows.push(['Descripción', '<span style="white-space:pre-wrap">'+esc(r.descripcion)+'</span>']);
|
||||||
|
return rows.map(([k,v])=>`<div class="row"><div class="k">${k}</div><div class="v">${v}</div></div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
const rows = [];
|
const rows = [];
|
||||||
rows.push(['Origen', `<span class="src-tag" style="background:${srcColor}">${esc(srcLabel)}</span>`]);
|
rows.push(['Origen', `<span class="src-tag" style="background:${srcColor}">${esc(srcLabel)}</span>`]);
|
||||||
if(e.codigo) rows.push(['Código', `<b>${esc(e.codigo)}</b>`]);
|
if(e.codigo) rows.push(['Código', `<b>${esc(e.codigo)}</b>`]);
|
||||||
@@ -839,7 +1134,11 @@ document.addEventListener('keydown', e=>{
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(e.key==='Escape'){ closeEvent(); return; }
|
if(e.key==='Escape'){
|
||||||
|
if ($('ev-form-modal').classList.contains('open')) closeEventForm();
|
||||||
|
else closeEvent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(e.key==='ArrowLeft') navPrev();
|
if(e.key==='ArrowLeft') navPrev();
|
||||||
if(e.key==='ArrowRight') navNext();
|
if(e.key==='ArrowRight') navNext();
|
||||||
if(e.key==='t'||e.key==='T') navToday();
|
if(e.key==='t'||e.key==='T') navToday();
|
||||||
|
|||||||
Reference in New Issue
Block a user