1062 lines
44 KiB
HTML
1062 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 id="previewVirtualLinkWrapper" class="d-none mt-2">
|
|
<a id="previewVirtualButton" href="#" target="_blank" class="btn btn-sm btn-success d-inline-flex align-items-center">
|
|
<i class="bi bi-camera-video me-1"></i>{% trans %}Unirse a Videollamada (Zoom / Meet){% endtrans %}
|
|
</a>
|
|
</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');
|
|
}
|
|
|
|
// Enlace de Aula Virtual / Videollamada
|
|
var vLink = ext.virtual_link;
|
|
var vBtnWrap = document.getElementById('previewVirtualLinkWrapper');
|
|
var vBtn = document.getElementById('previewVirtualButton');
|
|
if (vLink && vBtnWrap && vBtn) {
|
|
vBtn.href = vLink;
|
|
vBtnWrap.classList.remove('d-none');
|
|
} else if (vBtnWrap) {
|
|
vBtnWrap.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 %} |