Files
admin-edu-space/app/templates/schedule/calendar.html
T

1046 lines
44 KiB
HTML

{% extends "base.html" %}
{% block title %}{% trans %}Reservations Calendar{% endtrans %} - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<!-- Header -->
<div class="row mb-4 align-items-center">
<div class="col-md-7">
<h1 class="h3 mb-1">
<i class="bi bi-calendar3 text-primary me-2"></i>{% trans %}Reservations Calendar{% endtrans %}
</h1>
<p class="text-muted small mb-0">{% trans %}View & Manage Reservations{% endtrans %} — {% trans %}Horarios de cursada y asignación de aulas UniCABA{% endtrans %}</p>
</div>
<div class="col-md-5 text-md-end mt-3 mt-md-0">
{% if current_user.has_permission('reservations', 'read_write') %}
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-primary me-2">
<i class="bi bi-plus-circle me-1"></i>{% trans %}Add Reservation{% endtrans %}
</a>
{% endif %}
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-secondary">
<i class="bi bi-list me-1"></i>{% trans %}List View{% endtrans %}
</a>
</div>
</div>
<!-- Filters Toolbar (Classroom & Subject Filters to prevent overlapping) -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-3">
<div class="row g-2 align-items-center">
<!-- Filter by Classroom -->
<div class="col-lg-3 col-md-6">
<label for="filterClassroom" class="form-label small fw-bold text-muted mb-1">
<i class="bi bi-door-open text-success me-1"></i>{% trans %}Aula{% endtrans %}
</label>
<select id="filterClassroom" class="form-select form-select-sm">
<option value="">{% trans %}Todas las aulas{% endtrans %}</option>
{% for c in classrooms %}
<option value="{{ c.id }}">{{ c.code }} ({{ c.building }})</option>
{% endfor %}
</select>
</div>
<!-- Filter by Subject -->
<div class="col-lg-3 col-md-6">
<label for="filterSubject" class="form-label small fw-bold text-muted mb-1">
<i class="bi bi-book text-primary me-1"></i>{% trans %}Materia{% endtrans %}
</label>
<select id="filterSubject" class="form-select form-select-sm">
<option value="">{% trans %}Todas las materias{% endtrans %}</option>
{% for s in subjects %}
<option value="{{ s.id }}">{{ s.name }} [{{ s.code }}]</option>
{% endfor %}
</select>
</div>
<!-- Filter by Career -->
<div class="col-lg-3 col-md-6">
<label for="filterCareer" class="form-label small fw-bold text-muted mb-1">
<i class="bi bi-mortarboard text-info me-1"></i>{% trans %}Carrera{% endtrans %}
</label>
<select id="filterCareer" class="form-select form-select-sm">
<option value="">{% trans %}Todas las carreras{% endtrans %}</option>
{% for cr in careers %}
<option value="{{ cr.id }}">{{ cr.name }}</option>
{% endfor %}
</select>
</div>
<!-- Filter by Turno (Mañana / Tarde) -->
<div class="col-lg-3 col-md-6">
<label for="filterShift" class="form-label small fw-bold text-muted mb-1">
<i class="bi bi-brightness-high text-warning me-1"></i>{% trans %}Turno{% endtrans %}
</label>
<select id="filterShift" class="form-select form-select-sm">
<option value="">{% trans %}Todos los turnos{% endtrans %}</option>
<option value="Mañana">{% trans %}Turno Mañana{% endtrans %}</option>
<option value="Tarde">{% trans %}Turno Tarde / Vespertino{% endtrans %}</option>
</select>
</div>
</div>
<!-- Second row of filters: Building, Floor & Hide Virtual -->
<div class="row g-2 align-items-center mt-2 pt-2 border-top">
<!-- Filter by Building -->
<div class="col-lg-3 col-md-4">
<label for="filterBuilding" class="form-label small fw-bold text-muted mb-1">
<i class="bi bi-buildings text-primary me-1"></i>{% trans %}Filtrar por Edificio{% endtrans %}
</label>
<select id="filterBuilding" class="form-select form-select-sm">
<option value="">{% trans %}Todos los edificios{% endtrans %}</option>
{% for b in buildings %}
<option value="{{ b.name }}">{{ b.name }}</option>
{% endfor %}
</select>
</div>
<!-- Filter by Floor -->
<div class="col-lg-3 col-md-4">
<label for="filterFloor" class="form-label small fw-bold text-muted mb-1">
<i class="bi bi-layers text-secondary me-1"></i>{% trans %}Filtrar por Piso{% endtrans %}
</label>
<select id="filterFloor" class="form-select form-select-sm">
<option value="">{% trans %}Todos los pisos{% endtrans %}</option>
{% for f in available_floors %}
<option value="{{ f }}">{% if f == '0' or f == 0 or f == 'PB' %}{% trans %}Planta Baja{% endtrans %}{% else %}{% trans %}Piso{% endtrans %} {{ f }}{% endif %}</option>
{% endfor %}
</select>
</div>
<!-- Hide Virtual Classrooms Toggle -->
<div class="col-lg-4 col-md-4 pt-md-3">
<div class="form-check form-switch pt-1">
<input class="form-check-input" type="checkbox" role="switch" id="filterHideVirtual">
<label class="form-check-label fw-semibold small text-dark" for="filterHideVirtual">
<i class="bi bi-laptop me-1 text-primary"></i>{% trans %}Ocultar Aulas Virtuales{% endtrans %}
</label>
</div>
</div>
<!-- Reset Filters Button -->
<div class="col-lg-2 col-md-12 text-md-end pt-md-3">
<button type="button" class="btn btn-outline-secondary btn-sm w-100 w-md-auto" id="btnResetFilters">
<i class="bi bi-x-circle me-1"></i>{% trans %}Limpiar{% endtrans %}
</button>
</div>
</div>
<!-- Active filter indicator & counter -->
<div class="d-flex align-items-center flex-wrap gap-2 mt-2 pt-2 border-top">
<small class="text-muted fw-semibold">
<i class="bi bi-funnel-fill text-secondary me-1"></i>{% trans %}Estado{% endtrans %}:
</small>
<span id="activeFilterBadge" class="badge bg-secondary-subtle text-secondary border small">
{% trans %}Mostrando todas las aulas y materias{% endtrans %}
</span>
<span class="badge badge-brand ms-auto small">
<i class="bi bi-calendar-check me-1"></i><span id="eventCountNumber">0</span> {% trans %}clases visibles{% endtrans %}
</span>
</div>
</div>
</div>
<!-- Calendar Card -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-body-tertiary border-0 py-3">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div class="d-flex align-items-center gap-3">
<span class="fw-bold small text-secondary text-uppercase tracking-wide">
<i class="bi bi-info-circle me-1"></i>{% trans %}Referencia{% endtrans %}:
</span>
<div class="d-flex align-items-center">
<span class="badge badge-brand me-1" style="width: 14px; height: 14px; border-radius: 4px;"></span>
<small class="text-muted">{% trans %}Clase Confirmada{% endtrans %}</small>
</div>
<div class="d-flex align-items-center">
<span class="badge bg-warning me-1" style="width: 14px; height: 14px; border-radius: 4px;"></span>
<small class="text-muted">{% trans %}Pendiente{% endtrans %}</small>
</div>
</div>
<div class="text-muted small">
<i class="bi bi-lightning-charge-fill text-warning me-1"></i>{% trans %}Haz clic en cualquier clase para ver los detalles completos{% endtrans %}
</div>
</div>
</div>
<div class="card-body p-3">
<!-- FullCalendar Container -->
<div id="calendar"></div>
</div>
</div>
<!-- Modal Interactivo al Clic (Estilo Google Calendar Preview & Edición Rápida) -->
<div class="modal fade" id="eventPreviewModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width: 490px;">
<div class="modal-content border-0 shadow-lg gc-modal-card">
<!-- Banner Artístico Superior con Botones Rápidos (Estilo Google Calendar) -->
<div class="gc-modal-banner" id="gcModalBanner">
<div class="d-flex justify-content-end align-items-center gap-2 p-2 gc-banner-actions">
<a id="btnPreviewEdit" href="#" class="btn btn-sm btn-icon text-white" title="{% trans %}Editar / Ver Ficha{% endtrans %}">
<i class="bi bi-pencil fs-6"></i>
</a>
{% if current_user.has_permission('reservations', 'read_write') %}
<form id="formPreviewCancel" method="POST" class="d-inline" onsubmit="return confirm('{% trans %}¿Confirma que desea cancelar esta reserva?{% endtrans %}');">
<button type="submit" class="btn btn-sm btn-icon text-white" title="{% trans %}Cancelar Reserva{% endtrans %}">
<i class="bi bi-trash fs-6"></i>
</button>
</form>
{% endif %}
<button type="button" class="btn btn-sm btn-icon text-white" data-bs-dismiss="modal" aria-label="Close">
<i class="bi bi-x-lg fs-6"></i>
</button>
</div>
</div>
<!-- Contenido de la Tarjeta Google Calendar -->
<div class="modal-body p-4 pt-3">
<!-- Fila Principal con Cuadrado de Color y Título en Grande -->
<div class="d-flex align-items-start gap-3 mb-3">
<div class="gc-color-box rounded-2 mt-1 shadow-sm" id="gcPreviewColorBox" style="width: 22px; height: 22px; background-color: #B43E8E; flex-shrink: 0;"></div>
<div class="flex-grow-1 min-w-0">
<h4 class="h5 fw-bold mb-1 text-break text-body" id="previewSubjectName">Nombre de Asignatura</h4>
<div class="d-flex align-items-center flex-wrap gap-2">
<span class="badge font-monospace bg-secondary-subtle text-secondary border" id="previewSubjectCode">ASIG001</span>
<span class="badge" id="previewStatusBadge">Confirmada</span>
<span class="badge bg-secondary-subtle text-secondary border d-none" id="previewShiftBadge">Turno Tarde</span>
</div>
</div>
</div>
<!-- Lista de Atributos con Iconografía Limpia -->
<div class="d-flex flex-column gap-3 ps-2 gc-info-list">
<!-- 🕒 Horario y Fecha -->
<div class="d-flex align-items-center gap-3">
<div class="gc-icon-circle text-muted">
<i class="bi bi-clock fs-5"></i>
</div>
<div>
<div class="fw-semibold text-body" id="previewTimeRange">Lunes, 1 de septiembre · 17:30 a 21:30 hs</div>
<small class="text-muted" id="previewDateFull">Horario regular de cursada</small>
</div>
</div>
<!-- 📍 Espacio / Aula / Virtual Desglosado -->
<div class="d-flex align-items-start gap-3">
<div class="gc-icon-circle text-danger mt-1" id="previewLocationIcon">
<i class="bi bi-geo-alt fs-5"></i>
</div>
<div class="flex-grow-1">
<div class="d-flex flex-wrap gap-1 mb-1" id="previewLocationBadges">
<span class="badge bg-primary-subtle text-primary border" id="previewBadgeBuilding">🏢 Edificio Central</span>
<span class="badge bg-secondary-subtle text-secondary border" id="previewBadgeFloor">🪜 Piso 3</span>
<span class="badge bg-success-subtle text-success border" id="previewBadgeRoom">🚪 Aula 304</span>
</div>
<div id="previewVirtualWrapper" class="d-none">
<span class="badge bg-info-subtle text-info border">
<i class="bi bi-camera-video me-1"></i>{% trans %}Campus Virtual UniCABA (Teams / Moodle){% endtrans %}
</span>
</div>
</div>
</div>
<!-- 👤 Docente, Carrera y Comisión -->
<div class="d-flex align-items-center gap-3">
<div class="gc-icon-circle text-primary">
<i class="bi bi-person fs-5"></i>
</div>
<div>
<div class="fw-semibold text-body" id="previewCareer">Licenciatura en Sistemas</div>
<small class="text-muted">
<span id="previewCommission">Comisión 1</span>
<span class="mx-1">·</span>
<span id="previewTeacher">Docente Asignado</span>
<span class="mx-1">·</span>
<span id="previewAttendees">35 estudiantes</span>
</small>
</div>
</div>
<!-- 📝 Hito Académico / Detalle -->
<div class="d-flex align-items-start gap-3" id="previewNotesSection">
<div class="gc-icon-circle text-warning mt-1">
<i class="bi bi-flag fs-5"></i>
</div>
<div>
<div class="fw-semibold text-body" id="previewPurpose">Clase Regular</div>
<small class="text-muted text-break" id="previewNotes">Sin observaciones adicionales</small>
</div>
</div>
</div>
</div>
<!-- Footer de Acciones Rápidas -->
<div class="modal-footer border-0 p-3 pt-0 d-flex justify-content-between">
<a id="previewFullLink" href="#" class="btn btn-outline-primary btn-sm px-3">
<i class="bi bi-box-arrow-up-right me-1"></i>{% trans %}Ver Ficha Completa{% endtrans %}
</a>
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">
{% trans %}Cerrar{% endtrans %}
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.0/main.min.css" rel="stylesheet">
<style>
/* ==========================================================================
Google Calendar Aesthetic & FullCalendar Overrides (Light & Dark)
========================================================================== */
#calendar {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
min-height: 750px;
}
.fc .fc-toolbar-title {
font-size: 1.35rem !important;
font-weight: 700 !important;
color: var(--text-primary);
letter-spacing: -0.2px;
}
.fc .fc-button-primary {
background: var(--gradient-primary, linear-gradient(135deg, #B43E8E 0%, #9E367D 100%)) !important;
border: none !important;
border-radius: 6px !important;
font-weight: 600 !important;
font-size: 0.85rem !important;
padding: 0.35rem 0.75rem !important;
box-shadow: 0 2px 4px rgba(180, 62, 142, 0.25) !important;
}
.fc .fc-button-primary:hover {
background: linear-gradient(135deg, #9E367D 0%, #832765 100%) !important;
}
.fc .fc-button-primary:disabled {
background: #94A3B8 !important;
opacity: 0.6 !important;
}
.fc-theme-standard td, .fc-theme-standard th {
border-color: rgba(0, 0, 0, 0.08) !important;
}
[data-bs-theme="dark"] .fc-theme-standard td,
[data-bs-theme="dark"] .fc-theme-standard th {
border-color: rgba(255, 255, 255, 0.08) !important;
}
/* Column Headers (Google Calendar Style) */
.fc .fc-col-header-cell {
padding: 6px 0 !important;
background: transparent !important;
}
.fc .fc-col-header-cell-cushion {
color: var(--bs-body-color, #3c4043);
font-weight: 600;
font-size: 0.82rem;
text-decoration: none !important;
letter-spacing: 0.3px;
}
.fc-day-today .fc-col-header-cell-cushion {
color: #B43E8E !important;
font-weight: 700 !important;
}
/* Slot Lines & Label Height */
.fc .fc-timegrid-slot {
height: 28px !important;
}
.fc .fc-timegrid-slot-label-cushion {
color: var(--text-secondary, #64748B);
font-size: 0.72rem;
font-weight: 500;
}
/* Red Now Indicator Line */
.fc .fc-timegrid-now-indicator-line {
border-color: #ea4335 !important;
border-width: 2px !important;
z-index: 12 !important;
}
.fc .fc-timegrid-now-indicator-arrow {
border-color: #ea4335 !important;
border-top-color: transparent !important;
border-bottom-color: transparent !important;
}
/* Base Event Wrapper Overrides */
.fc-timegrid-event-harness {
margin: 0 !important;
}
.fc-timegrid-event, .fc-daygrid-event {
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
}
.fc-event-main {
padding: 0 !important;
height: 100% !important;
display: flex !important;
flex-direction: column !important;
}
/* -------------------------------------------------------------
Google Calendar Event Pill (.gc-event-pill)
------------------------------------------------------------- */
.gc-event-pill {
position: relative;
display: flex;
height: 100%;
width: 100%;
min-height: 26px;
border-radius: 6px;
background: rgba(180, 62, 142, 0.12);
border: 1px solid rgba(180, 62, 142, 0.28);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
overflow: hidden;
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.12s ease;
}
.gc-event-pill:hover {
transform: translateY(-1px) scale(1.01);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.18);
z-index: 20 !important;
filter: brightness(0.97);
}
[data-bs-theme="dark"] .gc-event-pill {
background: rgba(180, 62, 142, 0.22);
border-color: rgba(180, 62, 142, 0.45);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
}
[data-bs-theme="dark"] .gc-event-pill:hover {
filter: brightness(1.2);
}
/* Left Accent Stripe */
.gc-event-stripe {
width: 4px;
flex-shrink: 0;
height: 100%;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
/* Event Content Container */
.gc-event-content {
flex: 1;
min-width: 0;
padding: 2px 5px;
display: flex;
flex-direction: column;
justify-content: flex-start;
overflow: hidden;
}
/* Event Header: Time + Room Badge */
.gc-event-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 3px;
line-height: 1.1;
margin-bottom: 2px;
}
.gc-event-time {
font-size: 0.68rem;
font-weight: 700;
color: #7d2662;
white-space: nowrap;
}
[data-bs-theme="dark"] .gc-event-time {
color: #ffb1d9;
}
.gc-event-room {
font-size: 0.64rem !important;
font-weight: 600;
padding: 1px 4px !important;
border-radius: 4px;
background-color: rgba(180, 62, 142, 0.20) !important;
color: #63174c !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 70%;
line-height: 1.2;
}
[data-bs-theme="dark"] .gc-event-room {
background-color: rgba(255, 255, 255, 0.16) !important;
color: #ffffff !important;
}
.gc-event-room.gc-badge-virtual {
background-color: rgba(99, 102, 241, 0.20) !important;
color: #4338ca !important;
}
[data-bs-theme="dark"] .gc-event-room.gc-badge-virtual {
background-color: rgba(99, 102, 241, 0.35) !important;
color: #c7d2fe !important;
}
/* Subject Title (Compact with Ellipsis) */
.gc-event-title {
font-size: 0.78rem;
font-weight: 600;
line-height: 1.18;
color: #202124;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: -0.1px;
}
[data-bs-theme="dark"] .gc-event-title {
color: #f8f9fa;
}
/* Status Variants */
.gc-event-pill.status-pending {
background: rgba(245, 158, 11, 0.12);
border-color: rgba(245, 158, 11, 0.32);
}
.gc-event-pill.status-pending .gc-event-time {
color: #b45309;
}
[data-bs-theme="dark"] .gc-event-pill.status-pending {
background: rgba(245, 158, 11, 0.22);
}
[data-bs-theme="dark"] .gc-event-pill.status-pending .gc-event-time {
color: #fcd34d;
}
.gc-event-pill.status-virtual {
background: rgba(99, 102, 241, 0.12);
border-color: rgba(99, 102, 241, 0.30);
}
[data-bs-theme="dark"] .gc-event-pill.status-virtual {
background: rgba(99, 102, 241, 0.22);
}
/* -------------------------------------------------------------
Month View Pill (.gc-month-pill)
------------------------------------------------------------- */
.gc-month-pill {
display: flex;
align-items: center;
gap: 4px;
padding: 1px 6px;
margin: 1px 2px;
border-radius: 4px;
background: rgba(180, 62, 142, 0.12);
color: #202124;
font-size: 0.72rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
border-left: 3px solid var(--event-color, #B43E8E);
transition: filter 0.12s ease;
}
.gc-month-pill:hover {
filter: brightness(0.95);
}
[data-bs-theme="dark"] .gc-month-pill {
background: rgba(180, 62, 142, 0.25);
color: #f1f3f4;
}
.gc-month-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.gc-month-time {
font-size: 0.68rem;
font-weight: 700;
color: #7d2662;
flex-shrink: 0;
}
[data-bs-theme="dark"] .gc-month-time {
color: #ffb1d9;
}
.gc-month-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.gc-month-room {
font-size: 0.64rem;
color: #5f6368;
flex-shrink: 0;
}
[data-bs-theme="dark"] .gc-month-room {
color: #9aa0a6;
}
/* FullCalendar "+X más" Popover estilo Google Calendar */
.fc-popover {
border-radius: 12px !important;
border: none !important;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.22) !important;
background: var(--bs-body-bg, #ffffff) !important;
z-index: 1055 !important;
}
.fc-popover-header {
background: var(--bs-tertiary-bg, #f8f9fa) !important;
padding: 8px 12px !important;
border-top-left-radius: 12px !important;
border-top-right-radius: 12px !important;
font-weight: 700 !important;
font-size: 0.85rem !important;
}
.fc-more-link {
font-size: 0.72rem !important;
font-weight: 600 !important;
color: #B43E8E !important;
text-decoration: none !important;
padding: 1px 4px !important;
border-radius: 3px !important;
}
.fc-more-link:hover {
background-color: rgba(180, 62, 142, 0.12) !important;
}
/* -------------------------------------------------------------
Google Calendar Modal Card (.gc-modal-card)
------------------------------------------------------------- */
.gc-modal-card {
border-radius: 16px !important;
overflow: hidden !important;
}
.gc-modal-banner {
height: 80px;
background: linear-gradient(135deg, #B43E8E 0%, #832765 60%, #4338CA 100%);
position: relative;
background-size: cover;
}
.gc-modal-banner.banner-virtual {
background: linear-gradient(135deg, #4338CA 0%, #2563EB 60%, #0284C7 100%);
}
.gc-modal-banner.banner-pending {
background: linear-gradient(135deg, #D97706 0%, #F59E0B 100%);
}
.gc-banner-actions .btn-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.28);
border: none;
transition: background 0.15s ease, transform 0.15s ease;
}
.gc-banner-actions .btn-icon:hover {
background: rgba(0, 0, 0, 0.50);
transform: scale(1.05);
}
.gc-icon-circle {
width: 34px;
height: 34px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--bs-tertiary-bg, #f1f3f4);
flex-shrink: 0;
}
</style>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.0/main.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.0/locales-all.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var currentLang = '{{ session.get("language", "es") }}';
var todayDate = '{{ today_date or "2026-09-03" }}';
var filterRoom = document.getElementById('filterClassroom');
var filterSubj = document.getElementById('filterSubject');
var filterCar = document.getElementById('filterCareer');
var filterShift = document.getElementById('filterShift');
var filterBuilding = document.getElementById('filterBuilding');
var filterFloor = document.getElementById('filterFloor');
var filterHideVirtual = document.getElementById('filterHideVirtual');
var activeBadge = document.getElementById('activeFilterBadge');
var countEl = document.getElementById('eventCountNumber');
var days = ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado'];
var months = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];
function updateFilterSummaryBadge() {
var parts = [];
if (filterRoom.value && filterRoom.selectedOptions[0]) {
parts.push('Aula: ' + filterRoom.selectedOptions[0].text.split('(')[0].trim());
}
if (filterSubj.value && filterSubj.selectedOptions[0]) {
parts.push('Materia: ' + filterSubj.selectedOptions[0].text.split('[')[0].trim());
}
if (filterCar.value && filterCar.selectedOptions[0]) {
parts.push('Carrera: ' + filterCar.selectedOptions[0].text.trim());
}
if (filterBuilding && filterBuilding.value) {
parts.push('Edificio: ' + filterBuilding.value);
}
if (filterShift && filterShift.value) {
parts.push('Turno ' + filterShift.value);
}
if (filterFloor && filterFloor.value) {
parts.push(filterFloor.value === '0' ? 'Planta Baja' : 'Piso ' + filterFloor.value);
}
if (filterHideVirtual && filterHideVirtual.checked) {
parts.push('Sin Virtuales');
}
if (parts.length > 0) {
activeBadge.className = 'badge bg-primary text-white border small';
activeBadge.textContent = parts.join(' | ');
} else {
activeBadge.className = 'badge bg-secondary-subtle text-secondary border small';
activeBadge.textContent = 'Mostrando todas las aulas y materias';
}
}
var calendar = new FullCalendar.Calendar(calendarEl, {
locale: currentLang,
initialView: 'timeGridWeek',
initialDate: todayDate,
allDaySlot: false,
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
},
eventTimeFormat: {
hour: '2-digit',
minute: '2-digit',
hour12: false
},
slotMinTime: '07:00:00',
slotMaxTime: '23:00:00',
slotDuration: '00:30:00',
slotLabelInterval: '01:00:00',
nowIndicator: true,
height: 'auto',
/* Manejo de Eventos Simultáneos estilo Google Calendar */
dayMaxEvents: 3,
dayMaxEventRows: true,
eventMaxStack: 3,
moreLinkClick: 'popover',
slotEventOverlap: true,
expandRows: true,
stickyHeaderDates: true,
/* Dynamic Server-Side Filtered Events */
events: function(info, successCallback, failureCallback) {
var params = new URLSearchParams({
start: info.startStr,
end: info.endStr,
classroom_id: filterRoom.value || '',
subject_id: filterSubj.value || '',
career_id: filterCar.value || '',
shift: (filterShift ? filterShift.value : '') || '',
building: (filterBuilding ? filterBuilding.value : '') || '',
floor: (filterFloor ? filterFloor.value : '') || '',
hide_virtual: (filterHideVirtual && filterHideVirtual.checked) ? '1' : ''
});
updateFilterSummaryBadge();
fetch('{{ url_for("schedule.calendar_data") }}?' + params.toString())
.then(function(response) {
if (!response.ok) throw new Error('Network error');
return response.json();
})
.then(function(eventsData) {
if (countEl) {
countEl.textContent = eventsData.length;
}
successCallback(eventsData);
})
.catch(function(err) {
console.error('Error loading calendar events:', err);
failureCallback(err);
});
},
/* Diseño de Tarjetas de Evento Estilo Google Calendar */
eventContent: function(arg) {
var ext = arg.event.extendedProps || {};
var subjectName = ext.subject_name || arg.event.title || '';
var subjectCode = ext.subject_code || '';
var classroom = ext.classroom || '';
var isVirtual = ext.is_virtual || false;
var timeText = arg.timeText || '';
var status = (ext.status || 'CONFIRMED').toUpperCase();
var isMonthView = arg.view.type === 'dayGridMonth';
var eventColor = '#B43E8E';
if (status === 'PENDING') {
eventColor = '#F59E0B';
} else if (isVirtual) {
eventColor = '#6366F1';
}
// Vista de Mes: Píldora Google Calendar Compacta
if (isMonthView) {
var monthPill = document.createElement('div');
monthPill.className = 'gc-month-pill ' + (isVirtual ? 'gc-month-virtual' : '');
monthPill.style.setProperty('--event-color', eventColor);
monthPill.title = subjectName + (classroom ? ' · ' + classroom : '');
var startHour = timeText ? timeText.split(' - ')[0] : '';
monthPill.innerHTML = `
<span class="gc-month-dot" style="background-color: ${eventColor};"></span>
${startHour ? `<span class="gc-month-time">${startHour}</span>` : ''}
<span class="gc-month-title">${subjectName}</span>
<span class="gc-month-room">${isVirtual ? '<i class="bi bi-camera-video"></i>' : classroom}</span>
`;
return { domNodes: [monthPill] };
}
// Vista Semanal / Diaria: Tarjeta Píldora con Franja Lateral y Acento
var pill = document.createElement('div');
pill.className = 'gc-event-pill ' + (status === 'PENDING' ? 'status-pending' : '') + (isVirtual ? ' status-virtual' : '');
pill.style.setProperty('--event-color', eventColor);
pill.title = subjectName + (classroom ? ' - ' + classroom : '');
var stripe = `<div class="gc-event-stripe" style="background-color: ${eventColor};"></div>`;
var startHourOnly = timeText ? timeText.split(' - ')[0] : '';
var roomBadge = classroom ? `
<span class="gc-event-room badge ${isVirtual ? 'gc-badge-virtual' : ''}">
<i class="bi ${isVirtual ? 'bi-camera-video' : 'bi-door-open'} me-1"></i>${classroom}
</span>` : '';
var header = `
<div class="gc-event-header">
<span class="gc-event-time">${startHourOnly}</span>
${roomBadge}
</div>`;
var title = `<div class="gc-event-title" title="${subjectName}">${subjectName}</div>`;
pill.innerHTML = stripe + `<div class="gc-event-content">${header}${title}</div>`;
return { domNodes: [pill] };
},
/* Intercepción de Clic: Popover / Modal Flotante de Vista Previa Google Calendar */
eventClick: function(info) {
info.jsEvent.preventDefault();
var ext = info.event.extendedProps || {};
var subjectName = ext.subject_name || info.event.title || 'Asignatura';
var subjectCode = ext.subject_code || '-';
var classroom = ext.classroom || 'Sin Aula Asignada';
var building = ext.building || 'Edificio Central';
var career = ext.career || 'Tecnicatura / Licenciatura UniCABA';
var status = (ext.status || 'CONFIRMED').toUpperCase();
var isVirtual = ext.is_virtual || false;
var teacher = ext.teacher || 'Docente Responsable';
var commission = ext.commission ? `Comisión ${ext.commission}` : 'Comisión Regular';
var attendees = ext.attendees || 0;
var purpose = ext.purpose || 'Clase Regular';
var notes = ext.notes || 'Sin observaciones registradas';
var shift = ext.shift || '';
var eventColor = '#B43E8E';
var bannerClass = 'gc-modal-banner';
if (status === 'PENDING') {
eventColor = '#F59E0B';
bannerClass += ' banner-pending';
} else if (isVirtual) {
eventColor = '#6366F1';
bannerClass += ' banner-virtual';
}
// Actualizar Banner y Color Box
var bannerEl = document.getElementById('gcModalBanner');
if (bannerEl) bannerEl.className = bannerClass;
var colorBox = document.getElementById('gcPreviewColorBox');
if (colorBox) colorBox.style.backgroundColor = eventColor;
// Textos Principales
document.getElementById('previewSubjectName').textContent = subjectName;
document.getElementById('previewSubjectCode').textContent = subjectCode;
// Badges
var statusBadge = document.getElementById('previewStatusBadge');
if (statusBadge) {
if (status === 'CONFIRMED') {
statusBadge.className = 'badge bg-success-subtle text-success border border-success-subtle';
statusBadge.textContent = 'Confirmada';
} else if (status === 'PENDING') {
statusBadge.className = 'badge bg-warning-subtle text-warning border border-warning-subtle';
statusBadge.textContent = 'Pendiente';
} else {
statusBadge.className = 'badge bg-secondary-subtle text-secondary border';
statusBadge.textContent = status;
}
}
var shiftBadge = document.getElementById('previewShiftBadge');
if (shiftBadge) {
if (shift) {
shiftBadge.textContent = 'Turno ' + shift;
shiftBadge.classList.remove('d-none');
} else {
shiftBadge.classList.add('d-none');
}
}
// Horario Formateado
if (info.event.start) {
var start = info.event.start;
var end = info.event.end;
var dayName = days[start.getDay()];
var dayNum = start.getDate();
var monthName = months[start.getMonth()];
var startTime = start.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', hour12: false});
var endTime = end ? end.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', hour12: false}) : '';
document.getElementById('previewTimeRange').textContent = `${dayName}, ${dayNum} de ${monthName} · ${startTime}${endTime ? ' a ' + endTime + ' hs' : ''}`;
document.getElementById('previewDateFull').textContent = `Horario académico registrado`;
}
// Espacio Asignado Desglosado con Badges
var locIcon = document.getElementById('previewLocationIcon');
var virtualWrap = document.getElementById('previewVirtualWrapper');
var badgeB = document.getElementById('previewBadgeBuilding');
var badgeF = document.getElementById('previewBadgeFloor');
var badgeR = document.getElementById('previewBadgeRoom');
if (isVirtual) {
if (locIcon) locIcon.innerHTML = '<i class="bi bi-camera-video fs-5 text-primary"></i>';
if (badgeB) badgeB.textContent = '🏢 Campus Virtual';
if (badgeF) badgeF.textContent = '🪜 Remoto';
if (badgeR) badgeR.textContent = '🚪 ' + classroom;
if (virtualWrap) virtualWrap.classList.remove('d-none');
} else {
if (locIcon) locIcon.innerHTML = '<i class="bi bi-geo-alt fs-5 text-danger"></i>';
if (badgeB) badgeB.textContent = '🏢 ' + (building || 'Edificio Central');
if (badgeF) badgeF.textContent = '🪜 ' + (ext.floor || 'Planta Baja');
if (badgeR) badgeR.textContent = '🚪 ' + classroom;
if (virtualWrap) virtualWrap.classList.add('d-none');
}
// Carrera, Comisión, Docente y Asistentes
document.getElementById('previewCareer').textContent = career;
document.getElementById('previewCommission').textContent = commission;
document.getElementById('previewTeacher').textContent = teacher;
document.getElementById('previewAttendees').textContent = `${attendees} estudiantes esperados`;
// Hito / Notas
document.getElementById('previewPurpose').textContent = purpose;
document.getElementById('previewNotes').textContent = notes;
// Enlaces y Acciones
var detailUrl = info.event.url || ('/schedule/reservation/' + info.event.id + '/view');
var fullLink = document.getElementById('previewFullLink');
if (fullLink) fullLink.href = detailUrl;
var editBtn = document.getElementById('btnPreviewEdit');
if (editBtn) editBtn.href = detailUrl;
var cancelForm = document.getElementById('formPreviewCancel');
if (cancelForm) {
cancelForm.action = ext.cancel_url || ('/schedule/cancel/' + info.event.id);
}
// Mostrar Modal Google Calendar
var modal = new bootstrap.Modal(document.getElementById('eventPreviewModal'));
modal.show();
}
});
calendar.render();
// Event listeners to trigger immediate calendar refetch on filter change
filterRoom.addEventListener('change', function() {
calendar.refetchEvents();
});
filterSubj.addEventListener('change', function() {
calendar.refetchEvents();
});
filterCar.addEventListener('change', function() {
calendar.refetchEvents();
});
if (filterShift) {
filterShift.addEventListener('change', function() {
calendar.refetchEvents();
});
}
if (filterBuilding) {
filterBuilding.addEventListener('change', function() {
calendar.refetchEvents();
});
}
if (filterFloor) {
filterFloor.addEventListener('change', function() {
calendar.refetchEvents();
});
}
if (filterHideVirtual) {
filterHideVirtual.addEventListener('change', function() {
calendar.refetchEvents();
});
}
document.getElementById('btnResetFilters').addEventListener('click', function() {
filterRoom.value = '';
filterSubj.value = '';
filterCar.value = '';
if (filterShift) filterShift.value = '';
if (filterBuilding) filterBuilding.value = '';
if (filterFloor) filterFloor.value = '';
if (filterHideVirtual) filterHideVirtual.checked = false;
calendar.refetchEvents();
});
});
</script>
{% endblock %}