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

402 lines
26 KiB
HTML

{% extends "base.html" %}
{% block title %}{% trans %}Listado de Reservas{% 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-calendar-check text-primary me-2"></i>{% trans %}Listado de Reservas{% endtrans %}
</h1>
<p class="text-muted small mb-0">{% trans %}Registro histórico y programación completa de clases y asignación de aulas{% 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-success me-2">
<i class="bi bi-plus-circle me-1"></i>{% trans %}Nueva Reserva{% endtrans %}
</a>
{% endif %}
<a href="{{ url_for('schedule.calendar_view') }}" class="btn btn-outline-primary me-2">
<i class="bi bi-calendar3 me-1"></i>{% trans %}Ver Calendario{% endtrans %}
</a>
<a href="{{ url_for('schedule.today_schedule') }}" class="btn btn-outline-info">
<i class="bi bi-calendar-day me-1"></i>{% trans %}Cartelera de Hoy{% endtrans %}
</a>
</div>
</div>
<!-- Filters Card -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-3">
<form method="GET" action="{{ url_for('schedule.list_reservations') }}" class="row g-2 align-items-end">
<!-- Search text -->
<div class="col-lg-3 col-md-6">
<label class="form-label small fw-bold text-muted mb-1">
<i class="bi bi-search me-1"></i>{% trans %}Buscar{% endtrans %}
</label>
<input type="text" name="search" class="form-control form-control-sm" placeholder="Materia, código o aula..." value="{{ search or '' }}">
</div>
<!-- Status Filter -->
<div class="col-lg-2 col-md-3">
<label class="form-label small fw-bold text-muted mb-1">
<i class="bi bi-shield-check me-1"></i>{% trans %}Estado{% endtrans %}
</label>
<select name="status" class="form-select form-select-sm">
<option value="all" {% if status_filter == 'all' or not status_filter %}selected{% endif %}>{% trans %}Todos los estados{% endtrans %}</option>
<option value="confirmed" {% if status_filter == 'confirmed' %}selected{% endif %}>{% trans %}Confirmadas{% endtrans %}</option>
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>{% trans %}Pendientes{% endtrans %}</option>
<option value="cancelled" {% if status_filter == 'cancelled' %}selected{% endif %}>{% trans %}Canceladas{% endtrans %}</option>
</select>
</div>
<!-- Floor Filter -->
<div class="col-lg-2 col-md-3">
<label class="form-label small fw-bold text-muted mb-1">
<i class="bi bi-layers me-1"></i>{% trans %}Piso{% endtrans %}
</label>
<select name="floor" class="form-select form-select-sm">
<option value="">{% trans %}Todos los pisos{% endtrans %}</option>
{% for f in available_floors %}
<option value="{{ f }}" {% if floor_filter == f|string %}selected{% endif %}>
{% if f == 0 %}{% trans %}Planta Baja{% endtrans %}{% else %}{% trans %}Piso{% endtrans %} {{ f }}{% endif %}
</option>
{% endfor %}
</select>
</div>
<!-- Aula Filter -->
<div class="col-lg-3 col-md-4">
<label class="form-label small fw-bold text-muted mb-1">
<i class="bi bi-door-open text-primary me-1"></i>{% trans %}Aula / Espacio{% endtrans %}
</label>
<select name="classroom_id" class="form-select form-select-sm">
<option value="">{% trans %}Todas las aulas{% endtrans %}</option>
{% for c in all_classrooms %}
<option value="{{ c.id }}" {% if classroom_id == c.id %}selected{% endif %}>{{ c.building_name }} - {{ c.code }}</option>
{% endfor %}
</select>
</div>
<!-- Shift Filter -->
<div class="col-lg-2 col-md-3">
<label 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 name="shift" class="form-select form-select-sm">
<option value="">{% trans %}Todos los turnos{% endtrans %}</option>
<option value="Mañana" {% if shift_filter == 'Mañana' %}selected{% endif %}>{% trans %}Turno Mañana{% endtrans %}</option>
<option value="Tarde" {% if shift_filter == 'Tarde' %}selected{% endif %}>{% trans %}Turno Tarde{% endtrans %}</option>
</select>
</div>
<!-- Date Range From -->
<div class="col-lg-2 col-md-3">
<label class="form-label small fw-bold text-muted mb-1">
<i class="bi bi-calendar-event me-1"></i>{% trans %}Desde Fecha{% endtrans %}
</label>
<input type="date" name="date_from" class="form-control form-control-sm" value="{{ date_from or '' }}">
</div>
<!-- Date Range To -->
<div class="col-lg-2 col-md-3">
<label class="form-label small fw-bold text-muted mb-1">
<i class="bi bi-calendar-event me-1"></i>{% trans %}Hasta Fecha{% endtrans %}
</label>
<input type="date" name="date_to" class="form-control form-control-sm" value="{{ date_to or '' }}">
</div>
<!-- Row 2: Hide Virtual, Submit, Clear -->
<div class="col-lg-6 col-md-6 pt-2">
<div class="form-check form-switch pt-1">
<input class="form-check-input" type="checkbox" role="switch" name="hide_virtual" value="1" id="hideVirtualList" {% if hide_virtual %}checked{% endif %}>
<label class="form-check-label fw-semibold small text-dark" for="hideVirtualList">
<i class="bi bi-laptop me-1 text-primary"></i>{% trans %}Ocultar Aulas Virtuales{% endtrans %}
<span class="badge bg-secondary-subtle text-secondary border ms-1" style="font-size: 0.72rem;">{% trans %}Capacidad ilimitada{% endtrans %}</span>
</label>
</div>
</div>
<div class="col-lg-6 col-md-6 pt-2 text-md-end d-flex justify-content-md-end gap-2">
<button type="submit" class="btn btn-primary btn-sm px-3">
<i class="bi bi-funnel me-1"></i>{% trans %}Filtrar{% endtrans %}
</button>
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-secondary btn-sm px-3">
<i class="bi bi-arrow-counterclockwise me-1"></i>{% trans %}Limpiar{% endtrans %}
</a>
</div>
</form>
</div>
</div>
<!-- Header de Resumen y Selector de Vista -->
{% if reservations and reservations.items %}
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<div class="small text-muted">
<i class="bi bi-calendar-check text-primary me-1"></i>
<span>{% trans %}Total:{% endtrans %} <strong class="text-body">{{ reservations.total }}</strong> {% trans %}reservas encontradas{% endtrans %}</span>
</div>
<div class="d-flex align-items-center gap-2">
<span class="small text-muted d-none d-sm-inline">{% trans %}Modo de visualización:{% endtrans %}</span>
{% with context_id='reservations', default_view='table' %}
{% include 'partials/view_switcher.html' %}
{% endwith %}
</div>
</div>
<!-- 1. VISTA DE TABLA / GRILLA -->
<div data-view-context="reservations" data-view-container="table">
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 h6 fw-bold">
<i class="bi bi-table text-primary me-2"></i>{% trans %}Registros en Formato Tabla{% endtrans %}
</h5>
<small class="text-muted">{% trans %}Página{% endtrans %} {{ reservations.page }} {% trans %}de{% endtrans %} {{ reservations.pages }}</small>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light small text-uppercase">
<tr>
<th class="ps-3">{% trans %}Fecha y Horario{% endtrans %}</th>
<th>{% trans %}Aula / Piso{% endtrans %}</th>
<th>{% trans %}Asignatura / Código{% endtrans %}</th>
<th>{% trans %}Turno / Comisión{% endtrans %}</th>
<th>{% trans %}Capacidad{% endtrans %}</th>
<th>{% trans %}Estado{% endtrans %}</th>
<th class="text-end pe-3">{% trans %}Acciones{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for reservation in reservations.items %}
<tr>
<td class="ps-3">
<div class="fw-bold">{{ reservation.start_time.strftime('%d/%m/%Y') }}</div>
<small class="text-muted font-monospace">{{ reservation.start_time.strftime('%H:%M') }} - {{ reservation.end_time.strftime('%H:%M') }} hs</small>
</td>
<td>
<span class="badge bg-secondary-subtle text-secondary border font-monospace">
{{ reservation.classroom.code if reservation.classroom else 'Sin Aula' }}
</span>
<small class="text-muted d-block">
{% if reservation.classroom and reservation.classroom.floor == 0 %}PB{% elif reservation.classroom %}Piso {{ reservation.classroom.floor }}{% endif %}
{% if reservation.classroom and reservation.classroom.building %} · {{ reservation.classroom.building }}{% endif %}
</small>
</td>
<td>
<div class="fw-bold text-primary">{{ reservation.subject_name }}</div>
<small class="badge bg-secondary-subtle text-secondary font-monospace">{{ reservation.subject_code }}</small>
</td>
<td>
<span class="badge bg-info-subtle text-info border me-1">
{{ reservation.shift or 'Tarde' }}
</span>
<small class="text-muted">{{ reservation.commission.code if reservation.commission else 'C1' }}</small>
</td>
<td>
<span class="small">{{ reservation.expected_attendees }} / {{ reservation.classroom.capacity if reservation.classroom else '-' }}</span>
</td>
<td>
{% if reservation.status == 'CONFIRMED' or reservation.status.value == 'CONFIRMED' %}
<span class="badge bg-success-subtle text-success border border-success-subtle">
<i class="bi bi-check-circle me-1"></i>{% trans %}Confirmada{% endtrans %}
</span>
{% elif reservation.status == 'PENDING' or reservation.status.value == 'PENDING' %}
<span class="badge bg-warning-subtle text-warning border border-warning-subtle">
<i class="bi bi-clock me-1"></i>{% trans %}Pendiente{% endtrans %}
</span>
{% else %}
<span class="badge bg-danger-subtle text-danger border border-danger-subtle">
{% trans %}Cancelada{% endtrans %}
</span>
{% endif %}
</td>
<td class="text-end pe-3">
{% if reservation.effective_virtual_link %}
<a href="{{ reservation.effective_virtual_link }}" target="_blank"
class="btn btn-sm btn-outline-success py-0 px-2 me-1" title="{% trans %}Unirse a Clase Virtual (Zoom/Meet){% endtrans %}">
<i class="bi bi-camera-video"></i>
</a>
{% endif %}
{% if current_user.can_edit_reservation(reservation) %}
<a href="{{ url_for('schedule.edit_reservation', id=reservation.id) }}"
class="btn btn-sm btn-outline-warning py-0 px-2 me-1" title="{% trans %}Modificar Reserva{% endtrans %}">
<i class="bi bi-pencil"></i>
</a>
{% endif %}
<a href="{{ url_for('schedule.view_reservation', id=reservation.id) }}"
class="btn btn-sm btn-outline-secondary py-0 px-2" title="{% trans %}Ver Ficha{% endtrans %}">
<i class="bi bi-eye"></i>
</a>
{% if current_user.has_permission('reservations', 'read_write') and (reservation.status == 'PENDING' or reservation.status.value == 'PENDING') %}
<a href="{{ url_for('schedule.confirm_reservation', id=reservation.id) }}"
class="btn btn-sm btn-outline-success py-0 px-2 ms-1" title="{% trans %}Confirmar{% endtrans %}">
<i class="bi bi-check-lg"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 2. VISTA DE TARJETAS / MOSAICO -->
<div data-view-context="reservations" data-view-container="cards" class="d-none">
<div class="row g-3 mb-3">
{% for reservation in reservations.items %}
<div class="col-xxl-3 col-xl-4 col-lg-6 col-md-6 col-sm-12">
<div class="card border shadow-sm h-100 reservation-card">
<div class="card-body p-3 d-flex flex-column justify-content-between">
<div>
<!-- Header de tarjeta -->
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<span class="badge bg-primary text-white font-monospace mb-1">
<i class="bi bi-calendar-event me-1"></i>{{ reservation.start_time.strftime('%d/%m/%Y') }}
</span>
<div class="small fw-bold text-muted font-monospace">
<i class="bi bi-clock me-1"></i>{{ reservation.start_time.strftime('%H:%M') }} - {{ reservation.end_time.strftime('%H:%M') }} hs
</div>
</div>
{% if reservation.status == 'CONFIRMED' or reservation.status.value == 'CONFIRMED' %}
<span class="badge rounded-pill bg-success-subtle text-success border border-success-subtle">{% trans %}Confirmada{% endtrans %}</span>
{% elif reservation.status == 'PENDING' or reservation.status.value == 'PENDING' %}
<span class="badge rounded-pill bg-warning-subtle text-warning border border-warning-subtle">{% trans %}Pendiente{% endtrans %}</span>
{% else %}
<span class="badge rounded-pill bg-danger-subtle text-danger border border-danger-subtle">{% trans %}Cancelada{% endtrans %}</span>
{% endif %}
</div>
<!-- Asignatura y código -->
<h6 class="card-title fw-bold text-primary mb-1 text-truncate" title="{{ reservation.subject_name }}">
{{ reservation.subject_name }}
</h6>
<div class="d-flex flex-wrap gap-1 mb-2">
<span class="badge bg-secondary-subtle text-secondary font-monospace" style="font-size: 0.7rem;">
{{ reservation.subject_code }}
</span>
<span class="badge bg-info-subtle text-info border" style="font-size: 0.7rem;">
{{ reservation.shift or 'Tarde' }}
</span>
{% if reservation.commission %}
<span class="badge bg-body-tertiary text-body border" style="font-size: 0.7rem;">
{{ reservation.commission.name }}
</span>
{% endif %}
</div>
<!-- Ubicación / Aula -->
<div class="p-2 rounded bg-body-tertiary border mb-2 small">
<div class="d-flex align-items-center justify-content-between mb-1">
<span class="text-muted"><i class="bi bi-door-open text-primary me-1"></i>{% trans %}Espacio:{% endtrans %}</span>
{% if reservation.classroom %}
<span class="fw-bold font-monospace text-body">{{ reservation.classroom.code }}</span>
{% else %}
<span class="text-muted">{% trans %}Sin asignar{% endtrans %}</span>
{% endif %}
</div>
<div class="d-flex align-items-center justify-content-between small text-muted">
<span>{% trans %}Sede:{% endtrans %} {{ reservation.classroom.building_name if reservation.classroom else '-' }}</span>
<span>{{ reservation.classroom.floor_display if reservation.classroom else '-' }}</span>
</div>
</div>
<!-- Docente y Asistentes -->
<div class="small text-muted d-flex justify-content-between align-items-center mb-2">
<span class="text-truncate me-1" title="{{ reservation.user.name if reservation.user else 'No asignado' }}">
<i class="bi bi-person me-1"></i>{{ reservation.user.name if reservation.user else 'Sin docente' }}
</span>
<span>
<i class="bi bi-people me-1"></i>{{ reservation.expected_attendees }} {% trans %}alumnos{% endtrans %}
</span>
</div>
</div>
<!-- Acciones -->
<div class="d-flex justify-content-end gap-1 pt-2 border-top">
{% if reservation.effective_virtual_link %}
<a href="{{ reservation.effective_virtual_link }}" target="_blank"
class="btn btn-sm btn-outline-success py-1 px-2" title="{% trans %}Clase Virtual{% endtrans %}">
<i class="bi bi-camera-video"></i>
</a>
{% endif %}
<a href="{{ url_for('schedule.view_reservation', id=reservation.id) }}" class="btn btn-sm btn-outline-secondary py-1 px-2" title="{% trans %}Ver Ficha{% endtrans %}">
<i class="bi bi-eye"></i>
</a>
{% if current_user.can_edit_reservation(reservation) %}
<a href="{{ url_for('schedule.edit_reservation', id=reservation.id) }}" class="btn btn-sm btn-outline-warning py-1 px-2" title="{% trans %}Modificar{% endtrans %}">
<i class="bi bi-pencil"></i>
</a>
{% endif %}
{% if current_user.has_permission('reservations', 'read_write') and (reservation.status == 'PENDING' or reservation.status.value == 'PENDING') %}
<a href="{{ url_for('schedule.confirm_reservation', id=reservation.id) }}" class="btn btn-sm btn-outline-success py-1 px-2" title="{% trans %}Confirmar{% endtrans %}">
<i class="bi bi-check-lg"></i>
</a>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Paginación Compartida (visible en ambas vistas) -->
{% if reservations.pages > 1 %}
<div class="card border-0 shadow-sm py-2 px-3 d-flex flex-row justify-content-between align-items-center mb-3">
<small class="text-muted">{% trans %}Página{% endtrans %} {{ reservations.page }} {% trans %}de{% endtrans %} {{ reservations.pages }} ({{ reservations.total }} {% trans %}reservas{% endtrans %})</small>
<nav>
<ul class="pagination pagination-sm mb-0">
{% if reservations.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('schedule.list_reservations', page=reservations.prev_num, status=status_filter, date_from=date_from, date_to=date_to, floor=floor_filter, shift=shift_filter, classroom_id=classroom_id or '', hide_virtual='1' if hide_virtual else '', search=search) }}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{% endif %}
{% for p in reservations.iter_pages(left_edge=2, left_current=2, right_current=2, right_edge=2) %}
{% if p %}
{% if p == reservations.page %}
<li class="page-item active"><span class="page-link">{{ p }}</span></li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('schedule.list_reservations', page=p, status=status_filter, date_from=date_from, date_to=date_to, floor=floor_filter, shift=shift_filter, classroom_id=classroom_id or '', hide_virtual='1' if hide_virtual else '', search=search) }}">{{ p }}</a>
</li>
{% endif %}
{% else %}
<li class="page-item disabled"><span class="page-link">…</span></li>
{% endif %}
{% endfor %}
{% if reservations.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('schedule.list_reservations', page=reservations.next_num, status=status_filter, date_from=date_from, date_to=date_to, floor=floor_filter, shift=shift_filter, classroom_id=classroom_id or '', hide_virtual='1' if hide_virtual else '', search=search) }}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
{% else %}
<div class="card border-0 shadow-sm">
<div class="card-body text-center py-5">
<i class="bi bi-calendar-x text-muted fs-1 d-block mb-3"></i>
<h5 class="fw-bold">{% trans %}No se encontraron reservas{% endtrans %}</h5>
<p class="text-muted small mb-3">{% trans %}No hay registros que coincidan con los criterios de búsqueda o filtros seleccionados.{% endtrans %}</p>
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-primary btn-sm">
<i class="bi bi-arrow-counterclockwise me-1"></i>{% trans %}Restablecer Todos los Filtros{% endtrans %}
</a>
</div>
</div>
{% endif %}
</div>
{% endblock %}