Files
donna/views/calendario.html
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

1157 lines
56 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendario — AALN Donna</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--navy:#1B3A6B; --navy-d:#112446; --navy-l:#2D5090;
--gold:#C9A84C; --gold-l:#E8C96A; --gold-d:#9E7C2E;
--off:#F7F6F3;
--gray-100:#EEEDE9; --gray-200:#D6D4CC; --gray-400:#9B9A96;
--gray-700:#4A4946; --gray-900:#1A1916;
--red:#c0392b; --orange:#e67e22; --green:#27ae60; --blue:#2980b9;
--c-esc:#1B3A6B; --c-adm:#6c5b95; --c-corp:#9E7C2E; --c-mig:#2980b9; --c-lit:#722f37;
--shadow:0 1px 3px rgba(0,0,0,.06),0 4px 16px rgba(0,0,0,.06);
}
*,*::before,*::after { box-sizing:border-box; margin:0; padding:0; }
body { font-family:'DM Sans',system-ui,sans-serif; color:var(--gray-900); background:var(--off); font-size:13.5px; }
a { color:inherit; text-decoration:none; }
button { font-family:inherit; cursor:pointer; }
header { background:var(--navy); border-bottom:3px solid var(--gold); padding:0 24px;
display:flex; align-items:center; justify-content:space-between; height:58px;
position:sticky; top:0; z-index:200; }
.header-left { display:flex; align-items:center; gap:14px; }
.btn-back { display:inline-flex; align-items:center; gap:6px;
background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.18);
color:rgba(255,255,255,.9); padding:6px 12px 6px 9px; border-radius:8px;
font-size:12px; transition:all .15s; white-space:nowrap; }
.btn-back:hover { background:var(--gold); border-color:var(--gold); color:var(--navy); }
.btn-back svg { width:14px; height:14px; }
.logo { display:flex; align-items:center; gap:14px; }
.logo-mark { width:32px; height:32px; background:var(--gold); border-radius:6px;
display:flex; align-items:center; justify-content:center;
font-family:'DM Serif Display',serif; font-size:15px; color:var(--navy); }
.logo-text { display:flex; flex-direction:column; line-height:1.2; }
.logo-firm { font-size:11px; font-weight:600; color:var(--gold); letter-spacing:.08em; text-transform:uppercase; }
.logo-tool { font-size:13px; font-weight:400; color:rgba(255,255,255,.85); }
.header-right { display:flex; align-items:center; gap:16px; }
.sync-status { display:flex; align-items:center; gap:6px; font-size:11px; color:rgba(255,255,255,.55); }
.sync-dot { width:7px; height:7px; border-radius:50%; background:#4CAF50; animation:pulse 2.5s ease-in-out infinite; }
.sync-dot.syncing { background:var(--gold); }
.sync-dot.error { background:#ef5350; animation:none; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
.user-chip { display:flex; align-items:center; gap:8px; background:rgba(255,255,255,.1);
border:1px solid rgba(255,255,255,.15); border-radius:20px; padding:5px 12px 5px 6px; }
.user-avatar { width:24px; height:24px; border-radius:50%; background:var(--gold); color:var(--navy);
display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; }
.user-name { font-size:12px; color:rgba(255,255,255,.85); max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.btn-logout { font-size:11px; padding:4px 10px; border:1px solid rgba(255,255,255,.2); border-radius:6px;
background:transparent; color:rgba(255,255,255,.7); transition:all .15s; }
.btn-logout:hover { background:rgba(255,255,255,.1); color:#fff; }
.btn-gear, .btn-clock { display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px;
border:1px solid rgba(255,255,255,0.18); border-radius:8px; color:rgba(255,255,255,0.85);
transition:all .15s; text-decoration:none; cursor:pointer; font-family:'DM Sans',sans-serif; }
.btn-gear:hover, .btn-clock:hover { background:var(--gold); border-color:var(--gold); color:var(--navy); }
.btn-gear svg, .btn-clock svg { width:16px; height:16px; }
.cal-toolbar { background:#fff; border-bottom:1px solid var(--gray-100); padding:12px 24px;
display:flex; align-items:center; gap:14px; flex-wrap:wrap; box-shadow:var(--shadow); }
.view-tabs { display:flex; background:var(--gray-100); border-radius:8px; padding:2px; }
.view-tabs button { padding:6px 14px; background:transparent; border:none; border-radius:6px;
font-size:12.5px; color:var(--gray-700); transition:all .15s; }
.view-tabs button:hover { background:rgba(0,0,0,.04); }
.view-tabs button.active { background:var(--navy); color:#fff; }
.date-nav { display:flex; align-items:center; gap:6px; }
.date-nav button { width:30px; height:30px; border:1px solid var(--gray-200); background:#fff;
border-radius:6px; color:var(--gray-700); font-size:18px; transition:all .15s;
display:inline-flex; align-items:center; justify-content:center; }
.date-nav button:hover { background:var(--off); border-color:var(--navy-l); color:var(--navy); }
.date-nav .today-btn { width:auto; padding:0 12px; font-size:12px; font-weight:500; }
.period-label { font-family:'DM Serif Display',serif; font-size:18px; color:var(--navy);
margin-left:4px; min-width:200px; text-transform:capitalize; }
.search-box { flex:1; min-width:200px; max-width:300px; position:relative; }
.search-box input { width:100%; padding:7px 10px 7px 30px; border:1px solid var(--gray-200);
border-radius:8px; font-family:inherit; font-size:12.5px; background:var(--off); }
.search-box input:focus { outline:none; border-color:var(--navy-l); background:#fff;
box-shadow:0 0 0 3px rgba(27,58,107,.08); }
.search-box svg { position:absolute; left:9px; top:50%; transform:translateY(-50%);
width:14px; height:14px; color:var(--gray-400); pointer-events:none; }
.src-filters { display:flex; gap:6px; flex-wrap:wrap; }
.chip { display:inline-flex; align-items:center; gap:5px; padding:4px 10px; border-radius:20px;
font-size:11.5px; font-weight:600; cursor:pointer; user-select:none; transition:all .15s;
border:1.5px solid transparent; }
.chip .cdot { width:8px; height:8px; border-radius:50%; }
.chip.on { background:#fff; border-color:currentColor; }
.chip.off { background:var(--gray-100); color:var(--gray-400) !important; opacity:.65; }
.chip.off .cdot { background:var(--gray-400) !important; }
.chip[data-src="esc"] { color:var(--c-esc); }
.chip[data-src="adm"] { color:var(--c-adm); }
.chip[data-src="corp"] { color:var(--c-corp); }
.chip[data-src="mig"] { color:var(--c-mig); }
.chip[data-src="lit"] { color:var(--c-lit); }
.chip[data-src="manual"] { color:#27ae60; }
.chip[data-src="esc"] .cdot { background:var(--c-esc); }
.chip[data-src="adm"] .cdot { background:var(--c-adm); }
.chip[data-src="corp"] .cdot { background:var(--c-corp); }
.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-kpi { background:#fff; border:1px solid var(--gray-100); border-radius:10px; padding:12px 16px;
box-shadow:var(--shadow); border-left:4px solid var(--navy); }
.cal-kpi.warn { border-left-color:var(--orange); }
.cal-kpi.danger { border-left-color:var(--red); }
.cal-kpi-label { font-size:10.5px; font-weight:600; color:var(--gray-400); text-transform:uppercase; letter-spacing:.06em; }
.cal-kpi-val { font-family:'DM Serif Display',serif; font-size:26px; color:var(--navy); line-height:1.1; margin-top:2px; }
.cal-kpi.warn .cal-kpi-val { color:var(--orange); }
.cal-kpi.danger .cal-kpi-val { color:var(--red); }
@media (max-width:720px) { .cal-kpis { grid-template-columns:repeat(2,1fr); } }
.upcoming-strip { padding:14px 24px 0; overflow-x:auto; }
.upcoming-strip h4 { font-size:11px; font-weight:600; color:var(--gray-400); letter-spacing:.08em;
text-transform:uppercase; margin-bottom:8px; }
.upcoming-row { display:flex; gap:10px; min-width:fit-content; padding-bottom:4px; }
.up-card { flex:0 0 230px; background:#fff; border:1px solid var(--gray-100);
border-left:4px solid var(--orange); border-radius:8px; padding:8px 12px;
cursor:pointer; transition:all .15s; box-shadow:var(--shadow); }
.up-card.overdue { border-left-color:var(--red); }
.up-card:hover { transform:translateY(-1px); box-shadow:0 4px 16px rgba(17,36,70,.12); border-color:var(--navy-l); }
.up-card-date { font-family:'DM Serif Display',serif; font-size:13px; color:var(--navy); }
.up-card-rem { font-size:10.5px; font-weight:700; color:var(--orange); letter-spacing:.05em; text-transform:uppercase; margin-left:6px; }
.up-card.overdue .up-card-rem { color:var(--red); }
.up-card-title { font-size:12px; color:var(--gray-700); margin-top:4px; overflow:hidden;
text-overflow:ellipsis; white-space:nowrap; }
.up-card-meta { font-size:10.5px; color:var(--gray-400); margin-top:2px; overflow:hidden;
text-overflow:ellipsis; white-space:nowrap; }
.up-empty { padding:8px 14px; font-size:12px; color:var(--gray-400); }
main { padding:16px 24px 24px; }
.year-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:14px; }
@media (max-width:1100px) { .year-grid { grid-template-columns:repeat(3,1fr); } }
@media (max-width:780px) { .year-grid { grid-template-columns:repeat(2,1fr); } }
@media (max-width:480px) { .year-grid { grid-template-columns:1fr; } }
.year-mini { background:#fff; border:1px solid var(--gray-100); border-radius:10px; padding:12px;
cursor:pointer; transition:all .15s; box-shadow:var(--shadow); }
.year-mini:hover { transform:translateY(-2px); box-shadow:0 6px 20px rgba(17,36,70,.12); border-color:var(--navy-l); }
.year-mini-title { font-family:'DM Serif Display',serif; font-size:15px; color:var(--navy);
text-transform:capitalize; margin-bottom:8px; }
.year-mini-grid { display:grid; grid-template-columns:repeat(7,1fr); gap:2px; }
.year-mini-dow { font-size:9px; color:var(--gray-400); text-align:center; padding:2px 0;
font-weight:600; text-transform:uppercase; }
.year-mini-day { aspect-ratio:1; display:flex; align-items:center; justify-content:center;
font-size:10px; color:var(--gray-700); border-radius:3px; position:relative; }
.year-mini-day.dim { color:var(--gray-200); }
.year-mini-day.today { background:var(--navy); color:#fff; font-weight:700; }
.year-mini-day .dots { position:absolute; bottom:1px; left:50%; transform:translateX(-50%);
display:flex; gap:1px; }
.year-mini-day .dot { width:3px; height:3px; border-radius:50%; }
.year-mini-day.has-deadline::after {
content:''; position:absolute; top:1px; right:1px; width:5px; height:5px; border-radius:50%; background:var(--red);
}
.month-grid { background:var(--gray-200); border:1px solid var(--gray-200); border-radius:10px;
overflow:hidden; box-shadow:var(--shadow); display:grid;
grid-template-columns:repeat(7,1fr); gap:1px; }
.month-dow { background:var(--navy); color:rgba(255,255,255,.85); padding:8px;
font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.06em; text-align:center; }
.month-cell { background:#fff; min-height:118px; padding:6px; cursor:pointer;
transition:background .15s; display:flex; flex-direction:column; gap:2px; overflow:hidden; }
.month-cell:hover { background:#FAFBFD; }
.month-cell.dim { background:#F6F5F0; }
.month-cell.dim .cell-num { color:var(--gray-400); }
.month-cell.today { background:#FFFBEB; }
.month-cell .cell-num { font-size:11.5px; font-weight:600; color:var(--gray-700); margin-bottom:2px; }
.month-cell.today .cell-num { display:inline-flex; align-items:center; justify-content:center;
background:var(--navy); color:#fff; border-radius:50%;
width:22px; height:22px; font-size:11px; align-self:flex-start; }
.evt-chip { font-size:10.5px; padding:2px 6px; border-radius:4px; color:#fff; cursor:pointer;
overflow:hidden; white-space:nowrap; text-overflow:ellipsis; line-height:1.3;
border-left:3px solid rgba(0,0,0,.18); }
.evt-chip:hover { filter:brightness(1.1); }
.evt-chip.overdue { animation:flash 1.6s ease-in-out infinite; }
@keyframes flash { 0%,100%{filter:brightness(1)} 50%{filter:brightness(1.15)} }
.evt-more { font-size:10px; color:var(--gray-400); padding-left:2px; }
.week-grid { background:var(--gray-200); border:1px solid var(--gray-200); border-radius:10px;
overflow:hidden; box-shadow:var(--shadow); display:grid;
grid-template-columns:repeat(7,1fr); gap:1px; min-height:520px; }
.week-col { background:#fff; display:flex; flex-direction:column; }
.week-col-head { background:var(--navy); color:#fff; padding:10px 8px; text-align:center; }
.week-col-head.today { background:var(--gold-d); }
.week-col-day { font-size:10.5px; text-transform:uppercase; letter-spacing:.08em; opacity:.85; }
.week-col-num { font-family:'DM Serif Display',serif; font-size:22px; line-height:1.1; margin-top:2px; }
.week-col-body { padding:8px; flex:1; display:flex; flex-direction:column; gap:6px; cursor:pointer; }
.week-empty { font-size:11px; color:var(--gray-400); padding:6px; text-align:center; }
.week-evt { padding:7px 10px; border-radius:6px; color:#fff; cursor:pointer; font-size:11px;
border-left:4px solid rgba(0,0,0,.18); transition:transform .1s; }
.week-evt:hover { transform:translateY(-1px); }
.week-evt .tt { font-size:11px; font-weight:600; line-height:1.25; }
.week-evt .meta { font-size:10px; opacity:.85; margin-top:2px; }
.day-view { max-width:780px; margin:0 auto; }
.day-header { font-family:'DM Serif Display',serif; font-size:30px; color:var(--navy);
text-transform:capitalize; margin-bottom:6px; }
.day-sub { font-size:12px; color:var(--gray-400); margin-bottom:14px; }
.day-evt { display:grid; grid-template-columns:14px 1fr auto; gap:14px; background:#fff;
padding:14px 16px; margin-bottom:10px; border-radius:10px; box-shadow:var(--shadow);
cursor:pointer; transition:transform .15s; }
.day-evt:hover { transform:translateX(3px); }
.day-evt-bar { background:var(--navy); border-radius:14px; }
.day-evt-title { font-family:'DM Serif Display',serif; font-size:17px; color:var(--navy); line-height:1.2; }
.day-evt-meta { font-size:12px; color:var(--gray-700); margin-top:4px; }
.day-evt-badges { display:flex; flex-direction:column; align-items:flex-end; gap:4px; }
.evt-tag { font-size:10px; font-weight:700; padding:2px 8px; border-radius:20px;
letter-spacing:.05em; text-transform:uppercase; }
.cal-empty { text-align:center; padding:40px; color:var(--gray-400); font-size:13px;
background:#fff; border-radius:10px; border:1px dashed var(--gray-200); }
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(17,36,70,.6);
z-index:1000; align-items:center; justify-content:center; padding:24px; }
.modal-overlay.open { display:flex; }
.modal { background:#fff; border-radius:14px; width:100%; max-width:520px; max-height:90vh;
overflow:hidden; display:flex; flex-direction:column; box-shadow:0 20px 60px rgba(0,0,0,.3); }
.modal-head { background:var(--navy); color:#fff; padding:16px 22px; display:flex;
justify-content:space-between; align-items:center; border-bottom:2px solid var(--gold); }
.modal-head h3 { font-family:'DM Serif Display',serif; font-size:18px; font-weight:400; margin:0; max-width:84%; }
.modal-head .close { background:transparent; border:none; color:rgba(255,255,255,.7);
font-size:22px; line-height:1; }
.modal-head .close:hover { color:#fff; }
.modal-body { padding:18px 22px; overflow-y:auto; flex:1; }
.modal-body .row { display:flex; justify-content:space-between; align-items:flex-start; gap:14px;
padding:9px 0; border-bottom:1px solid var(--gray-100); }
.modal-body .row:last-child { border-bottom:none; }
.modal-body .row .k { font-size:10.5px; font-weight:700; color:var(--navy);
text-transform:uppercase; letter-spacing:.08em; min-width:130px; flex-shrink:0; }
.modal-body .row .v { font-size:13px; color:var(--gray-900); text-align:right; flex:1; word-break:break-word; }
.modal-body .src-tag { display:inline-block; padding:3px 10px; border-radius:20px; font-size:10.5px;
font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:#fff; }
.modal-foot { padding:14px 22px; border-top:1px solid var(--gray-100); background:var(--off);
display:flex; justify-content:space-between; gap:10px; }
.btn-secondary { padding:8px 16px; background:transparent; color:var(--gray-700);
border:1px solid var(--gray-200); border-radius:8px; font-size:13px; }
.btn-secondary:hover { background:var(--gray-100); }
.btn-primary { padding:8px 20px; background:var(--navy); color:#fff; border:none;
border-radius:8px; font-size:13px; font-weight:500; }
.btn-primary:hover { background:var(--navy-d); }
.toast { position:fixed; bottom:24px; right:24px; background:var(--navy-d); color:#fff;
padding:11px 18px; border-radius:8px; font-size:12.5px; border-left:3px solid var(--gold);
box-shadow:0 4px 20px rgba(0,0,0,.2); transform:translateY(20px); opacity:0;
transition:all .25s; z-index:2000; }
.toast.show { transform:translateY(0); opacity:1; }
.toast.error { border-left-color:#ef5350; }
footer.legend { text-align:center; padding:14px 24px 24px; font-size:11px; color:var(--gray-400); }
footer.legend .pill { display:inline-block; padding:2px 8px; border-radius:20px; color:#fff;
font-weight:600; margin:0 2px; font-size:10.5px; }
</style>
</head>
<body>
<header>
<div class="header-left">
<a class="btn-back" href="/" title="Volver al portal Donna">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
Portal
</a>
<div class="logo">
<div class="logo-mark">A</div>
<div class="logo-text">
<span class="logo-firm">AALN Law</span>
<span class="logo-tool">Calendario</span>
</div>
</div>
</div>
<div class="header-right">
<div class="sync-status">
<div class="sync-dot" id="sync-dot"></div>
<span id="sync-label">Conectando…</span>
</div>
<div class="user-chip">
<div class="user-avatar" id="user-avatar">?</div>
<span class="user-name" id="user-name">…</span>
</div>
<a class="btn-clock" href="/horas/" title="Reporte de horas">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</a>
<a class="btn-gear" href="/admin/" id="btn-gear" title="Panel de Control" style="display:none;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</a>
<a class="btn-logout" href="/auth/logout.php" onclick="return confirm('¿Cerrar sesión?');">Cerrar sesión</a>
</div>
</header>
<div class="cal-toolbar">
<div class="view-tabs" id="view-tabs">
<button data-view="year">Año</button>
<button data-view="month" class="active">Mes</button>
<button data-view="week">Semana</button>
<button data-view="day">Día</button>
</div>
<div class="date-nav">
<button id="nav-prev" title="Anterior (←)">‹</button>
<button class="today-btn" id="nav-today" title="Hoy (T)">Hoy</button>
<button id="nav-next" title="Siguiente (→)">›</button>
</div>
<span class="period-label" id="period-label">—</span>
<div class="search-box">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input type="search" id="search" placeholder="Buscar evento, cliente o abogado…">
</div>
<div class="src-filters" id="src-filters">
<span class="chip on" data-src="esc"><span class="cdot"></span>Escrituras</span>
<span class="chip on" data-src="adm"><span class="cdot"></span>Admin</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="lit"><span class="cdot"></span>Litigios</span>
<span class="chip on" data-src="manual"><span class="cdot"></span>Eventos</span>
</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 class="cal-kpis">
<div class="cal-kpi"><div class="cal-kpi-label">Total eventos</div><div class="cal-kpi-val" id="k-total">—</div></div>
<div class="cal-kpi warn"><div class="cal-kpi-label">Vencen ≤ 7 días</div><div class="cal-kpi-val" id="k-7d">—</div></div>
<div class="cal-kpi warn"><div class="cal-kpi-label">Vencen ≤ 30 días</div><div class="cal-kpi-val" id="k-30d">—</div></div>
<div class="cal-kpi danger"><div class="cal-kpi-label">Vencidos abiertos</div><div class="cal-kpi-val" id="k-overdue">—</div></div>
</div>
<div class="upcoming-strip">
<h4>Próximos vencimientos</h4>
<div class="upcoming-row" id="upcoming-row"><div class="up-empty">Cargando…</div></div>
</div>
<main id="cal-main"></main>
<footer class="legend">
<span class="pill" style="background:var(--c-esc)">Escrituras</span>
<span class="pill" style="background:var(--c-adm)">Admin</span>
<span class="pill" style="background:var(--c-corp)">Corp</span>
<span class="pill" style="background:var(--c-mig)">Migración</span>
<span class="pill" style="background:var(--c-lit)">Litigios</span>
<span class="pill" style="background:var(--orange)">≤7d</span>
<span class="pill" style="background:var(--red)">Vencido</span>
&nbsp;·&nbsp; Atajos: <b>← →</b> mueve período · <b>T</b> hoy · <b>Y/M/W/D</b> cambia vista · <b>Esc</b> cierra
</footer>
<div class="modal-overlay" id="evt-modal" onclick="if(event.target.id==='evt-modal')closeEvent()">
<div class="modal">
<header class="modal-head">
<h3 id="evt-title">Detalle del evento</h3>
<button class="close" onclick="closeEvent()" title="Cerrar (Esc)">✕</button>
</header>
<div class="modal-body" id="evt-body"></div>
<div class="modal-foot" id="evt-foot">
<button class="btn-secondary" onclick="closeEvent()">← Regresar al calendario</button>
<button class="btn-primary" id="btn-edit">Editar evento</button>
</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>
<script>
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', manual:'Evento'};
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_UPCOMING = '#e67e22';
const MES_ES = ['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre'];
const DOW_FULL = ['lunes','martes','miércoles','jueves','viernes','sábado','domingo'];
const DOW_SHORT = ['Lun','Mar','Mié','Jue','Vie','Sáb','Dom'];
const CLOSED_STATUS = new Set(['Culminado','Facturado','Inscrito']);
let events = [];
let view = 'month';
let cursor = stripTime(new Date());
let filters = new Set(['esc','adm','corp','mig','lit','manual']);
let editingEvId = null; // id del evento manual siendo editado (null = nuevo)
let search = '';
let selected = null;
let authUser = { email:'', name:'' };
// Gear icon solo visible para socios (window.DONNA inyectado por Bootstrap)
if (window.DONNA && window.DONNA.isSocio) {
document.addEventListener('DOMContentLoaded', () => {
const g = document.getElementById('btn-gear');
if (g) g.style.display = 'inline-flex';
});
}
function $(id){ return document.getElementById(id); }
function esc(s){ return (s==null?'':String(s)).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function stripTime(d){ const o=new Date(d); o.setHours(0,0,0,0); return o; }
function ymd(d){
if(!d) return '';
if(typeof d==='string'){
if(/^\d{4}-\d{2}-\d{2}/.test(d)) return d.slice(0,10);
d = new Date(d);
}
if(isNaN(d)) return '';
const m=String(d.getMonth()+1).padStart(2,'0');
const day=String(d.getDate()).padStart(2,'0');
return `${d.getFullYear()}-${m}-${day}`;
}
function parseYMD(s){
if(!s) return null;
const [y,m,d] = s.split('-').map(Number);
if(!y) return null;
return new Date(y, (m||1)-1, d||1);
}
function addDays(d, n){ const o=new Date(d); o.setDate(o.getDate()+n); return o; }
function addMonths(d, n){ return new Date(d.getFullYear(), d.getMonth()+n, 1); }
function startOfMonth(d){ return new Date(d.getFullYear(), d.getMonth(), 1); }
function startOfWeek(d){ const day=(d.getDay()+6)%7; return addDays(d, -day); }
function sameDate(a,b){ return ymd(a)===ymd(b); }
function daysToToday(s){
const today = stripTime(new Date());
const date = parseYMD(s);
if(!date) return null;
return Math.round((date - today)/(1000*60*60*24));
}
function setSync(state, label){
const dot=$('sync-dot'); if(!dot) return;
dot.className='sync-dot'+(state!=='ok'?' '+state:'');
$('sync-label').textContent=label;
}
function toast(msg, type='ok'){
const el=$('toast'); el.textContent=msg;
el.className='toast show'+(type==='error'?' error':'');
setTimeout(()=>el.className='toast'+(type==='error'?' error':''),3000);
}
function initials(text){
const parts=String(text||'').replace(/[@._-]/g,' ').trim().split(/\s+/);
return ((parts[0]||'').charAt(0)+(parts[1]||'').charAt(0)).toUpperCase()||'?';
}
function setUserChip(){
const email=authUser.email||''; const name=authUser.name||email;
if($('user-avatar')) $('user-avatar').textContent=initials(name||email);
if($('user-name')) { $('user-name').textContent=name||email||'Conectado'; $('user-name').title=email; }
}
async function apiCall(url, opts){
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;
try { j = await res.json(); } catch(e){ throw new Error('JSON inválido'); }
if(res.status===401 && j.auth_required){
window.location.href = j.login_url || '/auth/login.php?return_to=' + encodeURIComponent(location.pathname);
return null;
}
if(!res.ok || !j.ok) throw new Error(j.error || ('HTTP '+res.status));
if(j.user) authUser = j.user;
return j.data;
}
function titleOf(t, dept){
return t.nombre || t.tipo_tramite || t.tipo_proceso || (DEPT_LABELS[dept]||'Trámite');
}
function flatten(escrituras, tramites, manualEvents){
const out = [];
for(const e of (escrituras||[])){
if(e.fecha_solicitud){
out.push({
key:'esc-sol-'+e.id, source:'esc', type:'solicitud',
date: ymd(e.fecha_solicitud),
title: e.tramite || 'Trámite sin nombre',
cliente:e.cliente, abogado:e.abogado, estatus:e.estatus,
raw:e, ref_id:e.id,
nav: '/escrituras/?edit='+e.id,
});
}
}
for(const dept of ['adm','corp','mig','lit']){
for(const t of (tramites?.[dept]||[])){
const navUrl = '/tramites/?edit='+encodeURIComponent(t.codigo);
if(t.fecha_solicitud){
out.push({
key:'tra-sol-'+t.id, source:dept, type:'solicitud',
date: ymd(t.fecha_solicitud),
title: titleOf(t, dept),
cliente:t.cliente, abogado:t.abogado, estatus:t.estatus,
raw:t, ref_id:t.id, codigo:t.codigo, nav:navUrl,
});
}
if(dept==='mig' && t.fecha_vencimiento){
out.push({
key:'tra-venc-'+t.id, source:'mig', type:'vencimiento',
date: t.fecha_vencimiento,
title: titleOf(t, 'mig')+' — vencimiento',
cliente:t.cliente, abogado:t.abogado, estatus:t.estatus,
raw:t, ref_id:t.id, codigo:t.codigo, nav:navUrl,
});
}
if(dept==='lit' && t.fecha_termino){
out.push({
key:'tra-term-'+t.id, source:'lit', type:'termino',
date: t.fecha_termino,
title: titleOf(t, 'lit')+' — término procesal',
cliente:t.cliente, abogado:t.abogado, estatus:t.estatus,
raw:t, ref_id:t.id, codigo:t.codigo, nav:navUrl,
});
}
}
}
// 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
.filter(e => e.date)
.map(e => Object.assign(e, { daysOut: daysToToday(e.date) }))
.sort((a,b)=> a.date.localeCompare(b.date));
}
async function loadAll(){
setSync('syncing','Cargando…');
try {
const [escData, traData, manData] = await Promise.all([
apiCall('/escrituras/api.php?action=list'),
apiCall('/tramites/api.php?action=list'),
apiCall('/calendario/api.php?action=list'),
]);
if(escData===null || traData===null || manData===null) return;
events = flatten(escData, traData, manData);
setUserChip();
setSync('ok', events.length + ' eventos cargados');
render();
updateKPIs();
updateUpcoming();
} catch(e){
setSync('error','Sin conexión');
toast('Error al cargar: '+e.message,'error');
}
}
function visibleEvents(){
const q = search.toLowerCase().trim();
return events.filter(e=>{
if(!filters.has(e.source)) return false;
if(q){
const blob = ((e.title||'')+' '+(e.cliente||'')+' '+(e.abogado||'')+' '+(e.codigo||'')).toLowerCase();
if(!blob.includes(q)) return false;
}
return true;
});
}
function eventsOnDate(d){
const k = ymd(d);
return visibleEvents().filter(e => e.date===k);
}
function isDeadline(e){ return e.type==='vencimiento' || e.type==='termino'; }
function isOpen(e){ return !CLOSED_STATUS.has(e.estatus||''); }
function colorFor(e){
if(isDeadline(e) && e.daysOut!==null && isOpen(e)){
if(e.daysOut<0) return COLOR_OVERDUE;
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';
}
function badgeFor(e){
if(isDeadline(e) && e.daysOut!==null && isOpen(e)){
if(e.daysOut<0) return {label:'Vencido', bg:COLOR_OVERDUE};
if(e.daysOut===0) return {label:'Hoy', bg:COLOR_UPCOMING};
if(e.daysOut<=7) return {label:'≤'+e.daysOut+'d', bg:COLOR_UPCOMING};
}
return null;
}
function updateKPIs(){
const v = visibleEvents();
const deadlines = v.filter(e => isDeadline(e) && isOpen(e));
$('k-total').textContent = v.length;
$('k-7d').textContent = deadlines.filter(e => e.daysOut!==null && e.daysOut>=0 && e.daysOut<=7).length;
$('k-30d').textContent = deadlines.filter(e => e.daysOut!==null && e.daysOut>=0 && e.daysOut<=30).length;
$('k-overdue').textContent = deadlines.filter(e => e.daysOut!==null && e.daysOut<0).length;
}
function updateUpcoming(){
const wrap = $('upcoming-row');
const deadlines = visibleEvents()
.filter(e => isDeadline(e) && isOpen(e) && e.daysOut!==null)
.sort((a,b)=>a.daysOut-b.daysOut)
.slice(0,8);
if(!deadlines.length){
wrap.innerHTML='<div class="up-empty">No hay vencimientos próximos en los filtros actuales.</div>';
return;
}
wrap.innerHTML = deadlines.map(e=>{
const over = e.daysOut<0;
const rem = over ? 'Hace '+Math.abs(e.daysOut)+'d'
: e.daysOut===0 ? 'HOY'
: 'En '+e.daysOut+'d';
const d = parseYMD(e.date);
const ds = d ? d.toLocaleDateString('es-PA',{day:'2-digit',month:'short'}) : e.date;
return `<div class="up-card ${over?'overdue':''}" onclick='openEvent("${e.key}")'>
<div class="up-card-date">${esc(ds)} <span class="up-card-rem">${esc(rem)}</span></div>
<div class="up-card-title" title="${esc(e.title)}">${esc(e.title)}</div>
<div class="up-card-meta">${esc(e.cliente||'—')} · ${esc(e.abogado||'—')}</div>
</div>`;
}).join('');
}
function setView(v){
view = v;
document.querySelectorAll('#view-tabs button').forEach(b=>b.classList.toggle('active', b.dataset.view===v));
render();
}
function setCursor(d){ cursor = stripTime(d); render(); }
function navPrev(){
if(view==='year') cursor=new Date(cursor.getFullYear()-1, 0, 1);
else if(view==='month') cursor=addMonths(cursor,-1);
else if(view==='week') cursor=addDays(cursor,-7);
else if(view==='day') cursor=addDays(cursor,-1);
setCursor(cursor);
}
function navNext(){
if(view==='year') cursor=new Date(cursor.getFullYear()+1, 0, 1);
else if(view==='month') cursor=addMonths(cursor,1);
else if(view==='week') cursor=addDays(cursor,7);
else if(view==='day') cursor=addDays(cursor,1);
setCursor(cursor);
}
function navToday(){ setCursor(new Date()); }
function periodLabel(){
if(view==='year') return String(cursor.getFullYear());
if(view==='month') return MES_ES[cursor.getMonth()] + ' ' + cursor.getFullYear();
if(view==='week'){
const a=startOfWeek(cursor); const b=addDays(a,6);
return a.getDate()+' '+MES_ES[a.getMonth()].slice(0,3)+' — '+b.getDate()+' '+MES_ES[b.getMonth()].slice(0,3)+' '+b.getFullYear();
}
return DOW_FULL[(cursor.getDay()+6)%7] + ' ' + cursor.getDate() + ' de ' + MES_ES[cursor.getMonth()] + ' ' + cursor.getFullYear();
}
function render(){
$('period-label').textContent = periodLabel();
if(view==='year') renderYear();
else if(view==='month') renderMonth();
else if(view==='week') renderWeek();
else renderDay();
}
function renderYear(){
const year = cursor.getFullYear();
const today = stripTime(new Date());
const byDate = {};
for(const e of visibleEvents()){ (byDate[e.date] = byDate[e.date] || []).push(e); }
const months = Array.from({length:12}, (_,m) => {
const start = startOfWeek(new Date(year, m, 1));
const cells = [];
for(let i=0;i<42;i++){
const d = addDays(start, i);
const inMonth = d.getMonth()===m && d.getFullYear()===year;
const key = ymd(d);
const evs = byDate[key]||[];
const hasDeadline = evs.some(e => isDeadline(e) && isOpen(e));
const dots = evs.slice(0,4).map(e =>
`<span class="dot" style="background:${colorFor(e)}"></span>`
).join('');
cells.push(`<div class="year-mini-day ${inMonth?'':'dim'} ${sameDate(d,today)?'today':''} ${hasDeadline?'has-deadline':''}">${d.getDate()}<span class="dots">${dots}</span></div>`);
}
return `<div class="year-mini" onclick="jumpToMonth(${m})">
<div class="year-mini-title">${MES_ES[m]}</div>
<div class="year-mini-grid">
${DOW_SHORT.map(d=>`<div class="year-mini-dow">${d}</div>`).join('')}
${cells.join('')}
</div>
</div>`;
}).join('');
$('cal-main').innerHTML = `<div class="year-grid">${months}</div>`;
}
function jumpToMonth(m){
cursor = new Date(cursor.getFullYear(), m, 1);
setView('month');
}
function renderMonth(){
const today = stripTime(new Date());
const start = startOfWeek(startOfMonth(cursor));
const cells = [];
for(let i=0;i<42;i++){
const d = addDays(start, i);
const inMonth = d.getMonth()===cursor.getMonth();
const isToday = sameDate(d, today);
const evs = eventsOnDate(d);
const max = 3;
const chips = evs.slice(0,max).map(e=>{
const b = badgeFor(e);
const cls = (b && b.bg===COLOR_OVERDUE) ? 'evt-chip overdue' : 'evt-chip';
const label = b ? (e.title+' · '+b.label) : e.title;
return `<div class="${cls}" style="background:${colorFor(e)}" onclick='event.stopPropagation();openEvent("${e.key}")' title="${esc(label)} — ${esc(e.cliente||'')}">${esc(e.title)}</div>`;
}).join('');
const more = evs.length>max ? `<div class="evt-more">+ ${evs.length-max} más</div>` : '';
cells.push(`<div class="month-cell ${inMonth?'':'dim'} ${isToday?'today':''}" onclick="goToDay('${ymd(d)}')">
<span class="cell-num">${d.getDate()}</span>
${chips}${more}
</div>`);
}
$('cal-main').innerHTML = `<div class="month-grid">
${DOW_SHORT.map(d=>`<div class="month-dow">${d}</div>`).join('')}
${cells.join('')}
</div>`;
}
function goToDay(ymdStr){
cursor = parseYMD(ymdStr);
setView('day');
}
function renderWeek(){
const today = stripTime(new Date());
const start = startOfWeek(cursor);
const cols = [];
for(let i=0;i<7;i++){
const d = addDays(start, i);
const isToday = sameDate(d, today);
const evs = eventsOnDate(d);
const items = evs.length ? evs.map(e=>{
const b = badgeFor(e);
return `<div class="week-evt" style="background:${colorFor(e)}" onclick='event.stopPropagation();openEvent("${e.key}")'>
<div class="tt">${esc(e.title)}</div>
<div class="meta">${esc(e.cliente||'—')}${b?' · '+esc(b.label):''}</div>
</div>`;
}).join('') : '<div class="week-empty">Sin eventos</div>';
cols.push(`<div class="week-col">
<div class="week-col-head ${isToday?'today':''}">
<div class="week-col-day">${DOW_SHORT[i]}</div>
<div class="week-col-num">${d.getDate()}</div>
</div>
<div class="week-col-body" onclick="goToDay('${ymd(d)}')">${items}</div>
</div>`);
}
$('cal-main').innerHTML = `<div class="week-grid">${cols.join('')}</div>`;
}
function renderDay(){
const today = stripTime(new Date());
const evs = eventsOnDate(cursor);
const isToday = sameDate(cursor, today);
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">
<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>`;
if(!evs.length){
html += `<div class="cal-empty">No hay eventos este día.</div>`;
} else {
html += evs.map(e=>{
const b = badgeFor(e);
const tag = b ? `<span class="evt-tag" style="background:${b.bg};color:#fff">${esc(b.label)}</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}")'>
<div class="day-evt-bar" style="background:${colorFor(e)}"></div>
<div>
<div class="day-evt-title">${esc(e.title)}</div>
<div class="day-evt-meta">${meta}</div>
</div>
<div class="day-evt-badges">${tag}${srcTag}</div>
</div>`;
}).join('');
}
html += '</div>';
$('cal-main').innerHTML = html;
}
function openEvent(key){
const e = events.find(x => x.key===key);
if(!e){ toast('Evento no encontrado','error'); return; }
selected = e;
$('evt-title').textContent = e.manual_titulo || e.title;
$('evt-body').innerHTML = renderEventBody(e);
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');
}
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(){
selected = null;
$('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){
const srcColor = (e.source==='manual') ? (MANUAL_TIPO_COLORS[e.type] || SRC_COLORS.manual) : SRC_COLORS[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 d = parseYMD(e.date);
const dStr = d ? d.toLocaleDateString('es-PA',{weekday:'long', day:'2-digit', month:'long', year:'numeric'}) : e.date;
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 = [];
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>`]);
rows.push([tEvt, `<b style="text-transform:capitalize">${esc(dStr)}</b>`]);
if(e.daysOut!==null && isDeadline(e)){
let lbl;
if(!isOpen(e)) lbl = `<span style="color:var(--gray-400)">Cerrado (${esc(e.estatus)})</span>`;
else if(e.daysOut<0) lbl = `<span style="color:${COLOR_OVERDUE}"><b>Vencido hace ${Math.abs(e.daysOut)} día(s)</b></span>`;
else if(e.daysOut===0) lbl = `<span style="color:${COLOR_UPCOMING}"><b>Vence HOY</b></span>`;
else lbl = `Faltan <b>${e.daysOut}</b> día(s)`;
rows.push(['Restante', lbl]);
}
rows.push(['Cliente', esc(e.cliente||'—')]);
rows.push(['Abogado', esc(e.abogado||'—')]);
rows.push(['Estatus', esc(e.estatus||'—')]);
if(e.source==='esc'){
if(r.notaria) rows.push(['Notaría', esc(r.notaria)]);
if(r.inscribe) rows.push(['Se inscribe', esc(r.inscribe)]);
if(r.altera_turno) rows.push(['Altera turno', esc(r.altera_turno)]);
if(r.entrada) rows.push(['Entrada', '<span style="font-variant-numeric:tabular-nums">'+esc(r.entrada)+'</span>']);
}
if(e.source==='adm' && r.institucion) rows.push(['Institución', esc(r.institucion)]);
if(e.source==='corp' && r.tipo_documento) rows.push(['Tipo doc.', esc(r.tipo_documento)]);
if(e.source==='mig'){
if(r.tipo_tramite) rows.push(['Tipo trámite', esc(r.tipo_tramite)]);
if(r.doc_completa) rows.push(['Doc. completa', esc(r.doc_completa)]);
if(r.permiso_trabajo) rows.push(['Permiso trabajo', esc(r.permiso_trabajo)]);
if(r.dependientes) rows.push(['Dependientes', esc(r.dependientes)]);
}
if(e.source==='lit'){
if(r.tipo_proceso) rows.push(['Tipo proceso', esc(r.tipo_proceso)]);
if(r.juzgado) rows.push(['Juzgado', esc(r.juzgado)]);
if(r.esfera) rows.push(['Esfera', esc(r.esfera)]);
if(r.instancia) rows.push(['Instancia', esc(r.instancia)]);
if(r.circuito) rows.push(['Circuito', esc(r.circuito)]);
}
if(r.instrucciones) rows.push(['Instrucciones', '<span style="white-space:pre-wrap">'+esc(r.instrucciones)+'</span>']);
if(r.observaciones) rows.push(['Observaciones', '<span style="white-space:pre-wrap">'+esc(r.observaciones)+'</span>']);
return rows.map(([k,v])=>`<div class="row"><div class="k">${k}</div><div class="v">${v}</div></div>`).join('');
}
document.querySelectorAll('#view-tabs button').forEach(b=>{
b.addEventListener('click', ()=> setView(b.dataset.view));
});
$('nav-prev').addEventListener('click', navPrev);
$('nav-next').addEventListener('click', navNext);
$('nav-today').addEventListener('click', navToday);
$('search').addEventListener('input', e=>{
search = e.target.value;
render(); updateKPIs(); updateUpcoming();
});
document.querySelectorAll('.chip[data-src]').forEach(ch=>{
ch.addEventListener('click', ()=>{
const s = ch.dataset.src;
if(filters.has(s)) filters.delete(s); else filters.add(s);
ch.classList.toggle('on', filters.has(s));
ch.classList.toggle('off', !filters.has(s));
render(); updateKPIs(); updateUpcoming();
});
});
document.addEventListener('keydown', e=>{
if(/^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) {
if(e.key==='Escape' && e.target.id==='search') {
e.target.value=''; search=''; render(); updateKPIs(); updateUpcoming();
}
return;
}
if(e.key==='Escape'){
if ($('ev-form-modal').classList.contains('open')) closeEventForm();
else closeEvent();
return;
}
if(e.key==='ArrowLeft') navPrev();
if(e.key==='ArrowRight') navNext();
if(e.key==='t'||e.key==='T') navToday();
if(e.key==='y'||e.key==='Y') setView('year');
if(e.key==='m'||e.key==='M') setView('month');
if(e.key==='w'||e.key==='W') setView('week');
if(e.key==='d'||e.key==='D') setView('day');
});
setInterval(()=>{ if(!$('evt-modal').classList.contains('open')) loadAll(); }, 60000);
loadAll();
</script>
</body>
</html>