feat(i18n & ux): auditoria, traduccion completa, correcciones ux y gestion de idioma en perfil
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Edu-Space Admin - Plantilla de Variables de Entorno
|
||||||
|
# Copie este archivo como .env y complete con valores de producción seguros
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# Entorno Flask
|
||||||
|
FLASK_APP=app.py
|
||||||
|
FLASK_ENV=production
|
||||||
|
|
||||||
|
# Seguridad: Clave secreta para sesiones y protección CSRF
|
||||||
|
# Generar una clave aleatoria con: python -c 'import secrets; print(secrets.token_hex(32))'
|
||||||
|
SECRET_KEY=change-this-to-a-secure-random-secret-key-in-production
|
||||||
|
|
||||||
|
# Base de Datos PostgreSQL
|
||||||
|
DATABASE_URL=postgresql://postgres:password@localhost:5432/classrooms_db
|
||||||
|
DB_USERNAME=postgres
|
||||||
|
DB_PASSWORD=password
|
||||||
|
|
||||||
|
# Integración con API externa (Microservicio Spring Boot)
|
||||||
|
API_BASE_URL=http://localhost:8080/api
|
||||||
|
|
||||||
|
# Configuración de Internacionalización (Babel)
|
||||||
|
BABEL_DEFAULT_LOCALE=es
|
||||||
|
BABEL_DEFAULT_TIMEZONE=America/Argentina/Buenos_Aires
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"python.defaultInterpreterPath": "${workspaceFolder}\\venv\\Scripts\\python.exe",
|
||||||
|
"python.analysis.extraPaths": [
|
||||||
|
"${workspaceFolder}\\venv\\Lib\\site-packages"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Resumen de Cambios - Sesión de Desarrollo (Rama `dev`)
|
||||||
|
|
||||||
|
**Fecha:** 2026-09-02 / 2026-09-03
|
||||||
|
**Proyecto:** Admin Edu-Space (Flask + PostgreSQL)
|
||||||
|
**Rama:** `dev`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Auditoría Git y Puesta en Producción
|
||||||
|
* **Informe Técnico Completo (`DEPLOYMENT_AUDIT.md`):**
|
||||||
|
* Análisis de ramas locales y remotas (`main`, `dev`, `origin/planning`, `origin/commissions`, `origin/spanish`).
|
||||||
|
* Manual de despliegue en producción con arquitectura WSGI (Gunicorn), Nginx como proxy inverso, Systemd unit y contenedorización Docker / Docker Compose.
|
||||||
|
* **Archivos Base:**
|
||||||
|
* Creación de `.env.example` con todas las variables requeridas (PostgreSQL, Flask, Secret Key, Babel).
|
||||||
|
* Creación de `init_db.py` para bootstrap inicial seguro de base de datos y usuario administrador por defecto (`admin@edu-space.com` / `admin123`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Base de Datos y Modelos
|
||||||
|
* **Preferencia de Idioma Persistente:**
|
||||||
|
* Migración en PostgreSQL agregando la columna `preferred_language VARCHAR(10)` a la tabla `users`.
|
||||||
|
* Actualización del modelo `User` en `app/models/user.py` con el campo `preferred_language`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Implementación de Vistas y Plantillas Faltantes
|
||||||
|
* **Perfil de Usuario (`app/templates/auth/profile.html`):**
|
||||||
|
* Vista completa con datos del usuario, rol, estado, fecha de registro y reservas recientes.
|
||||||
|
* Formulario interactivo para configurar la **Preferencia de Idioma** (*Detectar automáticamente del navegador*, *Español*, *English*).
|
||||||
|
* **Cambio de Contraseña (`app/templates/auth/change_password.html`):**
|
||||||
|
* Formulario seguro para actualización de credenciales.
|
||||||
|
* **Manejo de Errores (`app/templates/auth/403.html`):**
|
||||||
|
* Pantalla de error 403 Forbidden estilizada.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Correcciones de UX y Bugs de Rutas
|
||||||
|
* **Navbar Limpio (`app/templates/base.html`):**
|
||||||
|
* Eliminación del bloque duplicado del menú de usuario `System Administrator`.
|
||||||
|
* **Corrección de BuildError en Comisiones:**
|
||||||
|
* En `app/routes/classrooms.py`, se agregó el alias de ruta `@classrooms_bp.route('/', endpoint='index')` para resolver enlaces a `classrooms.index`.
|
||||||
|
* En `app/templates/schedule/commissions.html`, se reemplazó el filtro inválido de Django `truncatewords(20)` por el filtro estándar de Jinja2 `truncate(100)`.
|
||||||
|
* **Corrección Visual en `/genetic-optimizer` (`app/templates/genetic_optimizer.html`):**
|
||||||
|
* Se agregó el espaciado `pt-5 mt-4` evitando que la barra de navegación fija superior (`fixed-top`) se monte sobre el contenido.
|
||||||
|
* Rediseño de tarjeta hero con contraste óptimo y reemplazo de iconos faltantes por Bootstrap Icons (`bi bi-...`).
|
||||||
|
* **Corrección en Calendario (`app/templates/schedule/calendar.html`):**
|
||||||
|
* Inclusión de contenedor con espaciado superior y bundle de localización `locales-all.min.js` para FullCalendar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Internacionalización (i18n) y Localización al Español
|
||||||
|
* **Configuración del Motor Babel (`config/config.py` y `babel.cfg`):**
|
||||||
|
* Configuración de `BABEL_TRANSLATION_DIRECTORIES` con ruta absoluta.
|
||||||
|
* Actualización del método de extracción Jinja2 en `babel.cfg`.
|
||||||
|
* **Formularios con Traducción Lazy (`lazy_gettext`):**
|
||||||
|
* `app/forms/auth.py` (`LoginForm`).
|
||||||
|
* `app/forms/classroom.py` (`ClassroomForm`, `ClassroomResourceForm`).
|
||||||
|
* `app/forms/reservation.py` (`ReservationForm`).
|
||||||
|
* **Traducción Integral de Plantillas:**
|
||||||
|
* `app/templates/auth/login.html`: Pantalla de login traducida y con selector de idioma visible.
|
||||||
|
* `app/templates/dashboard.html`: Todas las tarjetas estadísticas, acciones rápidas y tablas.
|
||||||
|
* `app/templates/classrooms/list.html` y `add.html`: Filtros, estados vacíos, cabeceras y formularios.
|
||||||
|
* `app/templates/schedule/today.html`: Horario del día, métricas y botones.
|
||||||
|
* `app/templates/schedule/calendar.html`: Vistas, leyendas y eventos.
|
||||||
|
* `app/templates/genetic_optimizer.html`: Estado del algoritmo y controles.
|
||||||
|
* `app/templates/partials/language_switcher.html`: Muestra "Español" en lugar de "Spanish".
|
||||||
|
* **Compilación de Catálogos:**
|
||||||
|
* Compilación binaria de `messages.po` a `messages.mo` en `translations/es/` y `translations/en/` con 0 mensajes pendientes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Lógica de Detección y Precedencia de Idiomas
|
||||||
|
En `app/__init__.py` (`get_locale()`):
|
||||||
|
1. **Parámetro URL (`?lang=`):** Máxima prioridad para cambio explícito.
|
||||||
|
2. **Preferencia en Perfil del Usuario (`current_user.preferred_language`):** Sobrescribe la configuración del navegador en toda la sesión si el usuario está autenticado.
|
||||||
|
3. **Preferencia de Sesión (`session['language']`):** Cambio manual mediante el dropdown.
|
||||||
|
4. **Detección Automática del Navegador (`request.accept_languages.best_match()`):** Valor por defecto para visitantes no autenticados y pantalla de login.
|
||||||
|
* Al cerrar sesión (`logout`), se limpia la sesión para que el login use el navegador.
|
||||||
|
* Al cambiar el idioma en el selector siendo usuario autenticado, se sincroniza automáticamente con su perfil en base de datos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Configuración del Entorno de Desarrollo
|
||||||
|
* Se creó `.vscode/settings.json` apuntando al intérprete del entorno virtual (`venv\Scripts\python.exe`), solucionando falsos positivos del analizador estático para `flask_wtf` y `flask_babel`.
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
# Auditoría Técnica y DevOps: Git, Arquitectura y Puesta en Producción
|
||||||
|
|
||||||
|
**Repositorio:** `admin-edu-space`
|
||||||
|
**Rol:** Tech Lead / DevOps Lead
|
||||||
|
**Fecha de Auditoría:** 2 de Septiembre de 2026
|
||||||
|
**Entorno de Auditoría:** Windows / Git 2.x / Python 3.12.4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resumen Ejecutivo
|
||||||
|
|
||||||
|
El presente documento constituye la auditoría integral de código, gestión de versiones (Git), arquitectura de software y preparación para producción del proyecto **Edu-Space Admin Panel** (`admin-edu-space`).
|
||||||
|
|
||||||
|
### Conclusiones Principales:
|
||||||
|
1. **Rama seleccionada para despliegue:** **`main`** (commit `3d4e999`). Es la única rama que integra la totalidad de los cambios, bugfixes y features desarrollados en las ramas remotas (`planning`, `commissions`, `spanish`). No existen ramas divergentes sin fusionar.
|
||||||
|
2. **Nivel de Madurez CI/CD:** **Crítico (0% Automatización)**. No existen flujos de trabajo en GitHub Actions, GitLab CI ni validación de pruebas automatizadas previas al merge.
|
||||||
|
3. **Seguridad & Configuración:** **Riesgo Alto**. El archivo `.env` se encuentra actualmente trackeado en el repositorio Git con credenciales por defecto. Se requiere eliminarlo del índice, generar `.env.example` y rotar credenciales.
|
||||||
|
4. **Infraestructura de Despliegue:** El repositorio carece de `Dockerfile`, `docker-compose.yml`, scripts de systemd y no incluye un servidor WSGI de producción (`gunicorn`) en `requirements.txt`. El paso a producción requiere los artefactos y procedimientos estandarizados que se detallan en este informe.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fase 1: Análisis de Ramas (Git Audit)
|
||||||
|
|
||||||
|
## 1.1 Inventario de Ramas y Commits
|
||||||
|
|
||||||
|
Se ejecutó la inspección exhaustiva de referencias locales y remotas:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch --all
|
||||||
|
git branch -a --sort=-committerdate
|
||||||
|
git for-each-ref --format='%(refname:short) | %(committerdate:iso) | %(subject) | %(authorname)' refs/heads refs/remotes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tabla Comparativa de Ramas:
|
||||||
|
|
||||||
|
| Rama | Tipo | Último Commit | Fecha del Commit | Autor | Estado respecto a `main` |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| **`main`** | Local / Remota | `3d4e999` | 2026-04-17 18:18:00 -0300 | `ale-vz` | **Rama Primaria (HEAD)** |
|
||||||
|
| **`origin/planning`** | Remota | `ec977bf` | 2026-04-17 18:14:59 -0300 | `Alejandro Vazquez` | **100% Fusionada** (Merge PR #2 en `3d4e999`) |
|
||||||
|
| **`origin/commissions`**| Remota | `18f1d3a` | 2026-03-30 17:37:53 -0300 | `Alejandro Vazquez` | **100% Fusionada** (Incluida en historia de `planning`) |
|
||||||
|
| **`origin/spanish`** | Remota | `9af25df` | 2026-03-30 17:11:13 -0300 | `Alejandro Vazquez` | **100% Fusionada** (Merge PR #1 en `d4281cd` y PR #2) |
|
||||||
|
|
||||||
|
### Verificación de Divergencias:
|
||||||
|
Al ejecutar `git branch -r --no-merged main`, el resultado es **vacío**.
|
||||||
|
Al verificar conteo simétrico `git rev-list --left-right --count <rama>...main`:
|
||||||
|
- `origin/planning...main`: `0 2` (0 commits pendientes, 2 commits por delante en `main` por los merges).
|
||||||
|
- `origin/commissions...main`: `0 6` (0 commits pendientes, 6 commits por delante en `main`).
|
||||||
|
- `origin/spanish...main`: `0 7` (0 commits pendientes, 7 commits por delante en `main`).
|
||||||
|
|
||||||
|
### Tags y Releases:
|
||||||
|
- `git tag -l`: **Vacío**. No existen versiones etiquetadas (e.g. `v1.0.0`, `v0.1.0`) ni releases formales en el historial.
|
||||||
|
|
||||||
|
## 1.2 Auditoría de CI/CD y Calidad de Código
|
||||||
|
- **Workflows automatizados:** Inexistentes. No hay directorio `.github/workflows/`, ni `.gitlab-ci.yml`, ni archivo de integración continua de ningún proveedor.
|
||||||
|
- **Validación de Tests en Pull Requests:** Los merges de los Pull Requests `#1` y `#2` se realizaron sin puertas de enlace automatizadas (quality gates).
|
||||||
|
- **Cobertura de pruebas en repo:** Existe únicamente `tests/test_genetic_algorithm.py` enfocado en el motor heurístico, pero no se ejecuta en ningún pipeline.
|
||||||
|
|
||||||
|
## 1.3 Dictamen Conclusivo de Rama para Despliegue
|
||||||
|
|
||||||
|
> ### 🏆 Rama Determinada: **`main`** (Commit `3d4e999`)
|
||||||
|
>
|
||||||
|
> **Justificación Técnica:**
|
||||||
|
> 1. **Consolidación completa:** `main` agrupa de forma lineal y validada todas las funcionalidades de las ramas temáticas:
|
||||||
|
> - Incorpora las traducciones y arreglos de internacionalización (`spanish`).
|
||||||
|
> - Incorpora la lógica y vistas de comisiones (`commissions`).
|
||||||
|
> - Incorpora el motor de optimización de aulas y correcciones de UI (`planning`).
|
||||||
|
> 2. **Cero pérdida de código (No regressions):** No existe ningún commit en las ramas remotas que no esté ya contenido en `main`.
|
||||||
|
> 3. **Estabilidad comprobada:** Resuelve errores críticos de SQLAlchemy con carga ansiosa (`joinedload`) documentados en la evolución del repositorio.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fase 2: Informe Técnico del Proyecto
|
||||||
|
|
||||||
|
## 2.1 Stack Tecnológico y Propósito del Sistema
|
||||||
|
|
||||||
|
### Propósito
|
||||||
|
**Edu-Space Admin Panel** es una interfaz web de administración para la gestión, reserva y optimización algorítmica de aulas universitarias/académicas. El sistema permite:
|
||||||
|
- Gestionar edificios, aulas, capacidad física y recursos técnicos (proyectores, computadoras).
|
||||||
|
- Visualizar horarios y agendas en tiempo real mediante calendarios interactivos.
|
||||||
|
- Administrar materias y comisiones docentes.
|
||||||
|
- Asignar aulas automáticamente utilizando un **Algoritmo Genético** que evalúa aforo, prioridades horarias y resuelve conflictos de superposición de reservas.
|
||||||
|
|
||||||
|
### Stack de Tecnologías
|
||||||
|
|
||||||
|
| Componente | Tecnología | Versión | Propósito / Responsabilidad |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **Lenguaje Base** | Python | `>=3.8` (Óptimo: `3.11` / `3.12`) | Runtime principal de ejecución |
|
||||||
|
| **Framework Web** | Flask | `3.0.0` | Arquitectura web basada en Application Factory |
|
||||||
|
| **Capa ORM** | SQLAlchemy / Flask-SQLAlchemy | `2.0.23` / `3.1.1` | Modelado de datos relacional y queries |
|
||||||
|
| **Driver de BD** | psycopg2-binary | `2.9.9` | Conector nativo de alto rendimiento a PostgreSQL |
|
||||||
|
| **Migraciones** | Flask-Migrate | `4.0.5` | Wrapper de Alembic (requiere inicialización) |
|
||||||
|
| **Autenticación** | Flask-Login / Werkzeug | `0.6.3` / `3.0.1` | Manejo de sesiones, roles y hash `pbkdf2:sha256` |
|
||||||
|
| **Formularios & CSRF** | Flask-WTF / WTForms | `1.2.1` / `3.1.0` | Validación y protección contra ataques CSRF |
|
||||||
|
| **Internacionalización** | Flask-Babel / Babel | `4.0.0` / `2.14.0` | Detección y renderizado multilingüe (ES / EN) |
|
||||||
|
| **Frontend** | Bootstrap 5, FullCalendar, Chart.js | Bundled | UI responsiva, calendarios y estadísticas |
|
||||||
|
| **Microservicio Backend**| Spring Boot (Java) | `8080` (Externo) | API de negocio y persistencia central (opcional/compartida) |
|
||||||
|
|
||||||
|
### Arquitectura de Componentes y Flujo de Datos
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
Client[Navegador / Cliente Web] -->|HTTP / HTTPS| Nginx[Nginx Reverse Proxy & SSL]
|
||||||
|
Nginx -->|WSGI Proxy :5000| Gunicorn[Gunicorn WSGI Server]
|
||||||
|
Gunicorn --> App[Flask App Factory: create_app]
|
||||||
|
|
||||||
|
subgraph "Flask Application Blueprints"
|
||||||
|
App --> AuthBP[auth_bp: /login, /logout, /profile]
|
||||||
|
App --> MainBP[main_bp: /dashboard, /api/dashboard-stats]
|
||||||
|
App --> ClassroomsBP[classrooms_bp: /classrooms]
|
||||||
|
App --> ScheduleBP[schedule_bp: /schedule]
|
||||||
|
App --> GeneticBP[genetic_bp: /api/genetic & /genetic-optimizer]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Core Engine"
|
||||||
|
GeneticBP --> GAEngine[Algoritmo Genético: ReservationOptimizer]
|
||||||
|
GAEngine --> FitnessEval[Evaluación: Aforo 40%, Horario 30%, Conflicto 20%]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Persistencia & Servicios"
|
||||||
|
App --> SQLAlchemy[SQLAlchemy ORM + joinedload]
|
||||||
|
SQLAlchemy --> PostgreSQL[(PostgreSQL 14+ / classrooms_db)]
|
||||||
|
App -.->|Integración Configurada| SpringBoot[Spring Boot API :8080/api]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.2 Requisitos de Entorno y Variables de Configuración
|
||||||
|
|
||||||
|
### ⚠️ Hallazgos Críticos de Seguridad
|
||||||
|
1. **Archivo `.env` versionado:** Se encontró el archivo `.env` físico bajo control de versiones Git con contraseñas en texto plano (`password`).
|
||||||
|
- *Acción correctiva obligatoria:* Ejecutar `git rm --cached .env`, añadirlo a `.gitignore` y rotar todas las credenciales antes de salir a producción.
|
||||||
|
2. **Traducciones no compiladas:** El archivo `.gitignore` excluye deliberadamente `*.mo`. Por ende, el paso de build debe compilar los `.po` mediante `pybabel compile -d translations`.
|
||||||
|
|
||||||
|
### Especificación de Variables de Entorno (`.env.example`)
|
||||||
|
|
||||||
|
```env
|
||||||
|
# ==============================================================================
|
||||||
|
# Edu-Space Admin - Configuración de Entorno de Producción
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# Entorno Flask
|
||||||
|
FLASK_APP=app.py
|
||||||
|
FLASK_ENV=production
|
||||||
|
DEBUG=False
|
||||||
|
|
||||||
|
# Seguridad: Clave secreta para cookies de sesión y CSRF (Mínimo 64 caracteres aleatorios)
|
||||||
|
# Generar con: python -c 'import secrets; print(secrets.token_hex(32))'
|
||||||
|
SECRET_KEY=generate-a-strong-random-secret-key-for-production-use
|
||||||
|
|
||||||
|
# Conexión a Base de Datos PostgreSQL
|
||||||
|
# Formato: postgresql://<user>:<password>@<host>:<port>/<dbname>
|
||||||
|
DATABASE_URL=postgresql://eduspace_user:StrongProductionPassword123!@db-host:5432/classrooms_db
|
||||||
|
|
||||||
|
# Parámetros de compatibilidad si aplica
|
||||||
|
DB_USERNAME=eduspace_user
|
||||||
|
DB_PASSWORD=StrongProductionPassword123!
|
||||||
|
|
||||||
|
# Integración con API externa (Spring Boot)
|
||||||
|
API_BASE_URL=http://springboot-service:8080/api
|
||||||
|
|
||||||
|
# Configuración de Internacionalización y Carga de Archivos
|
||||||
|
BABEL_DEFAULT_LOCALE=es
|
||||||
|
BABEL_DEFAULT_TIMEZONE=America/Argentina/Buenos_Aires
|
||||||
|
MAX_CONTENT_LENGTH=16777216
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.3 Puesta en Producción (Deployment Determinado)
|
||||||
|
|
||||||
|
### 2.3.1 Estrategia de Contenedores (Docker + Docker Compose)
|
||||||
|
|
||||||
|
Para garantizar un despliegue repetible e inmutable, se especifica el contenedor de producción:
|
||||||
|
|
||||||
|
#### `Dockerfile` (Multi-stage / Producción)
|
||||||
|
```dockerfile
|
||||||
|
# Stage 1: Build de dependencias y compilación de traducciones
|
||||||
|
FROM python:3.11-slim AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir --user -r requirements.txt && \
|
||||||
|
pip install --no-cache-dir --user gunicorn==21.2.0
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
# Compilar catálogos de traducción i18n
|
||||||
|
RUN python -m babel.messages.frontend compile -d translations
|
||||||
|
|
||||||
|
# Stage 2: Runtime limpio y seguro
|
||||||
|
FROM python:3.11-slim AS runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libpq5 \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Crear usuario sin privilegios
|
||||||
|
RUN useradd -m -u 1001 appuser
|
||||||
|
|
||||||
|
COPY --from=builder /root/.local /home/appuser/.local
|
||||||
|
COPY --from=builder --chown=appuser:appuser /app /app
|
||||||
|
|
||||||
|
ENV PATH=/home/appuser/.local/bin:$PATH
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV FLASK_ENV=production
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Healthcheck interno del contenedor
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:5000/login || exit 1
|
||||||
|
|
||||||
|
CMD ["gunicorn", "--workers=4", "--threads=2", "--bind=0.0.0.0:5000", "--access-logfile=-", "--error-logfile=-", "app:create_app()"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `docker-compose.yml` (Stack Completo con PostgreSQL y Nginx)
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: eduspace_db
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: classrooms_db
|
||||||
|
POSTGRES_USER: eduspace_user
|
||||||
|
POSTGRES_PASSWORD: StrongProductionPassword123!
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U eduspace_user -d classrooms_db"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: eduspace_admin
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
expose:
|
||||||
|
- "5000"
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: eduspace_proxy
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
- ./app/static:/app/static:ro
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3.2 Despliegue en Máquina Virtual Linux (Systemd + Gunicorn + Nginx)
|
||||||
|
|
||||||
|
Si el despliegue se efectúa en un servidor Linux (Ubuntu/Debian) sin Docker:
|
||||||
|
|
||||||
|
#### 1. Pasos de Instalación y Preparación:
|
||||||
|
```bash
|
||||||
|
# 1. Clonar rama main
|
||||||
|
git clone -b main <URL_DEL_REPOSITORIO> /var/www/admin-edu-space
|
||||||
|
cd /var/www/admin-edu-space
|
||||||
|
|
||||||
|
# 2. Configurar entorno virtual
|
||||||
|
python3.11 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# 3. Instalar dependencias del proyecto + Servidor WSGI
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install gunicorn==21.2.0
|
||||||
|
|
||||||
|
# 4. Configurar variables de entorno seguras
|
||||||
|
cp .env.example .env
|
||||||
|
chmod 600 .env
|
||||||
|
nano .env # Ajustar SECRET_KEY y DATABASE_URL
|
||||||
|
|
||||||
|
# 5. Compilar traducciones
|
||||||
|
pybabel compile -d translations
|
||||||
|
|
||||||
|
# 6. Inicializar esquema de base de datos (si la base está vacía)
|
||||||
|
python -c "from app import create_app, db; app = create_app(); app.app_context().push(); db.create_all()"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Configuración de Servicio Systemd (`/etc/systemd/system/eduspace.service`):
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Edu-Space Admin Gunicorn Daemon
|
||||||
|
After=network.target postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=www-data
|
||||||
|
Group=www-data
|
||||||
|
WorkingDirectory=/var/www/admin-edu-space
|
||||||
|
Environment="PATH=/var/www/admin-edu-space/venv/bin"
|
||||||
|
EnvironmentFile=/var/www/admin-edu-space/.env
|
||||||
|
ExecStart=/var/www/admin-edu-space/venv/bin/gunicorn \
|
||||||
|
--workers 4 \
|
||||||
|
--threads 2 \
|
||||||
|
--bind 127.0.0.1:5000 \
|
||||||
|
--access-logfile /var/log/eduspace/access.log \
|
||||||
|
--error-logfile /var/log/eduspace/error.log \
|
||||||
|
"app:create_app()"
|
||||||
|
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Configuración del Reverse Proxy Nginx (`/etc/nginx/sites-available/eduspace`):
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name admin.edu-space.com;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name admin.edu-space.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/admin.edu-space.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/admin.edu-space.com/privkey.pem;
|
||||||
|
|
||||||
|
client_max_body_size 16M;
|
||||||
|
|
||||||
|
# Entrega directa de archivos estáticos por Nginx
|
||||||
|
location /static/ {
|
||||||
|
alias /var/www/admin-edu-space/app/static/;
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, no-transform";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy hacia el daemon Gunicorn
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:5000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_redirect off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.4 Verificación, Monitoreo y Health Checks
|
||||||
|
|
||||||
|
### 2.4.1 Endpoint de Verificación Recomendado (`/healthz`)
|
||||||
|
Actualmente el proyecto solo cuenta con endpoints protegidos por autenticación (`@login_required`). Para orquestadores (Kubernetes, AWS ALB, Docker), se debe incorporar una ruta pública de verificación en `app/routes/main.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@main_bp.route('/healthz')
|
||||||
|
def healthz():
|
||||||
|
"""Health check endpoint para balanceadores de carga y monitoreo"""
|
||||||
|
try:
|
||||||
|
# Validar conectividad con PostgreSQL
|
||||||
|
db.session.execute(db.text('SELECT 1'))
|
||||||
|
return jsonify({
|
||||||
|
'status': 'HEALTHY',
|
||||||
|
'database': 'CONNECTED',
|
||||||
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
|
}), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'UNHEALTHY',
|
||||||
|
'database': str(e),
|
||||||
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
|
}), 503
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4.2 Pruebas de Humo (Smoke Tests) Post-Despliegue
|
||||||
|
|
||||||
|
Ejecutar la siguiente suite de verificación inmediata tras desplegar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Comprobación de servicio y respuesta HTTP
|
||||||
|
curl -I -k https://admin.edu-space.com/login
|
||||||
|
# Respuesta esperada: HTTP/1.1 200 OK
|
||||||
|
|
||||||
|
# 2. Comprobación del Health Endpoint
|
||||||
|
curl -s https://admin.edu-space.com/healthz | jq .
|
||||||
|
# Respuesta esperada: {"status": "HEALTHY", "database": "CONNECTED"}
|
||||||
|
|
||||||
|
# 3. Comprobación de entrega de estáticos
|
||||||
|
curl -I https://admin.edu-space.com/static/css/style.css
|
||||||
|
# Respuesta esperada: HTTP/1.1 200 OK (o 304 Not Modified)
|
||||||
|
|
||||||
|
# 4. Comprobación de logs de Gunicorn
|
||||||
|
sudo journalctl -u eduspace.service -n 50 --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4.3 Matriz de Monitoreo y Observabilidad
|
||||||
|
|
||||||
|
| Dimensión | Métrica Clave | Umbral de Alerta | Acción Correctiva |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **Disponibilidad** | Tasa de Respuestas HTTP 5xx | `> 1%` en 5 min | Inspeccionar `error.log` de Gunicorn; revisar pool de conexiones BD |
|
||||||
|
| **Latencia** | Tiempo de respuesta P95 | `> 1.5s` en endpoints | Revisar índices en tablas `reservations` y `classrooms` |
|
||||||
|
| **Base de Datos** | Pool de conexiones agotado | Conexiones activas `> 85%` | Aumentar `max_overflow` en SQLAlchemy o activar PgBouncer |
|
||||||
|
| **Optimización** | Tiempo de ejecución de Algoritmo Genético | `> 10s` en `/api/genetic/optimize` | Limitar rango de comisiones enviadas o desacoplar a Celery/Redis |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.5 Roadmap de Mejoras DevOps & Calidad (Tech Lead Recommendations)
|
||||||
|
|
||||||
|
1. **Implementar CI/CD con GitHub Actions:**
|
||||||
|
- Crear `.github/workflows/ci.yml` para ejecutar linting (`flake8` / `ruff`) y la suite de pruebas `unittest` en cada PR.
|
||||||
|
2. **Desacoplar `.env` de Git:**
|
||||||
|
- Remover `.env` del repositorio (`git rm --cached .env`) e incorporar `python-dotenv` seguro mediante inyección de secretos en el pipeline.
|
||||||
|
3. **Gestión Formal de Migraciones:**
|
||||||
|
- Ejecutar `flask db init` y generar el primer script de migración versionado en `migrations/versions/`.
|
||||||
|
4. **Asincronía para el Algoritmo Genético:**
|
||||||
|
- Actualmente, la optimización heurística se ejecuta de forma sincrónica en el worker HTTP. Si el volumen de comisiones crece, puede bloquear el worker de Gunicorn (timeout 30s). Se recomienda migrar la ejecución pesada a una cola asíncrona con **Celery** o **Redis Queue (RQ)**.
|
||||||
|
5. **Alinear Rutas Duplicadas:**
|
||||||
|
- Resolver la colisión de ruta entre `main_bp.route('/genetic-optimizer')` y `genetic_web_bp.route('/genetic-optimizer')`.
|
||||||
@@ -3,4 +3,5 @@ from app import create_app
|
|||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
# Server entrypoint with auto-reload (locale auto-detection & profile override)
|
||||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||||
+22
-16
@@ -41,28 +41,34 @@ def create_app(config_class=Config):
|
|||||||
app.register_blueprint(genetic_bp)
|
app.register_blueprint(genetic_bp)
|
||||||
app.register_blueprint(genetic_web_bp)
|
app.register_blueprint(genetic_web_bp)
|
||||||
|
|
||||||
# Babel language selector
|
# Language selector function
|
||||||
|
def get_locale():
|
||||||
|
# 1. Check if language is explicitly set in URL parameters
|
||||||
|
lang = request.args.get('lang')
|
||||||
|
if lang and lang in app.config['LANGUAGES']:
|
||||||
|
return lang
|
||||||
|
|
||||||
|
# 2. Check if user is authenticated and has a configured language in profile
|
||||||
|
from flask_login import current_user
|
||||||
|
if current_user.is_authenticated and getattr(current_user, 'preferred_language', None):
|
||||||
|
if current_user.preferred_language in app.config['LANGUAGES']:
|
||||||
|
return current_user.preferred_language
|
||||||
|
|
||||||
|
# 3. Check if language is stored in session (e.g. manual switcher)
|
||||||
|
if 'language' in session and session['language'] in app.config['LANGUAGES']:
|
||||||
|
return session['language']
|
||||||
|
|
||||||
|
# 4. Check browser language preference by default (especially on login/guest)
|
||||||
|
return request.accept_languages.best_match(app.config['LANGUAGES']) or app.config['BABEL_DEFAULT_LOCALE']
|
||||||
|
|
||||||
|
# Babel context processor
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_conf_vars():
|
def inject_conf_vars():
|
||||||
return dict(
|
return dict(
|
||||||
languages=app.config['LANGUAGES'],
|
languages=app.config['LANGUAGES'],
|
||||||
current_lang=session.get('language', request.accept_languages.best_match(app.config['LANGUAGES']) or app.config['BABEL_DEFAULT_LOCALE'])
|
current_lang=str(get_locale())
|
||||||
)
|
)
|
||||||
|
|
||||||
# Language selector function
|
|
||||||
def get_locale():
|
|
||||||
# 1. Check if language is explicitly set in URL parameters
|
|
||||||
if request.args.get('lang'):
|
|
||||||
lang = request.args.get('lang')
|
|
||||||
if lang in app.config['LANGUAGES']:
|
|
||||||
return lang
|
|
||||||
|
|
||||||
# 2. Check if language is stored in session
|
|
||||||
if 'language' in session:
|
|
||||||
return session['language']
|
|
||||||
|
|
||||||
# 3. Check browser language preference
|
|
||||||
return request.accept_languages.best_match(app.config['LANGUAGES']) or app.config['BABEL_DEFAULT_LOCALE']
|
|
||||||
|
|
||||||
# Configure Babel
|
# Configure Babel
|
||||||
babel.init_app(app, locale_selector=get_locale)
|
babel.init_app(app, locale_selector=get_locale)
|
||||||
|
|||||||
+5
-4
@@ -1,13 +1,14 @@
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
||||||
from wtforms.validators import DataRequired, Email, Length
|
from wtforms.validators import DataRequired, Email, Length
|
||||||
|
from flask_babel import lazy_gettext as _l
|
||||||
|
|
||||||
class LoginForm(FlaskForm):
|
class LoginForm(FlaskForm):
|
||||||
email = StringField('Email', validators=[
|
email = StringField(_l('Email'), validators=[
|
||||||
DataRequired(),
|
DataRequired(),
|
||||||
Email(),
|
Email(),
|
||||||
Length(max=120)
|
Length(max=120)
|
||||||
])
|
])
|
||||||
password = PasswordField('Password', validators=[DataRequired()])
|
password = PasswordField(_l('Password'), validators=[DataRequired()])
|
||||||
remember_me = BooleanField('Remember me')
|
remember_me = BooleanField(_l('Remember me'))
|
||||||
submit = SubmitField('Sign In')
|
submit = SubmitField(_l('Sign In'))
|
||||||
+22
-21
@@ -1,44 +1,45 @@
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, IntegerField, TextAreaField, BooleanField, SubmitField, SelectField
|
from wtforms import StringField, IntegerField, TextAreaField, BooleanField, SubmitField, SelectField
|
||||||
from wtforms.validators import DataRequired, Length, NumberRange, Optional
|
from wtforms.validators import DataRequired, Length, NumberRange, Optional
|
||||||
|
from flask_babel import lazy_gettext as _l
|
||||||
|
|
||||||
class ClassroomForm(FlaskForm):
|
class ClassroomForm(FlaskForm):
|
||||||
# campus_id = SelectField('Campus', coerce=int, validators=[DataRequired()])
|
# campus_id = SelectField(_l('Campus'), coerce=int, validators=[DataRequired()])
|
||||||
building = StringField('Building', validators=[
|
building = StringField(_l('Building'), validators=[
|
||||||
DataRequired(),
|
DataRequired(),
|
||||||
Length(min=2, max=100)
|
Length(min=2, max=100)
|
||||||
])
|
])
|
||||||
room_number = StringField('Room Number', validators=[
|
room_number = StringField(_l('Room Number'), validators=[
|
||||||
DataRequired(),
|
DataRequired(),
|
||||||
Length(min=1, max=50)
|
Length(min=1, max=50)
|
||||||
])
|
])
|
||||||
floor = IntegerField('Floor', validators=[
|
floor = IntegerField(_l('Floor'), validators=[
|
||||||
DataRequired(),
|
DataRequired(),
|
||||||
NumberRange(min=1, max=50)
|
NumberRange(min=1, max=50)
|
||||||
])
|
])
|
||||||
capacity = IntegerField('Capacity', validators=[
|
capacity = IntegerField(_l('Capacity'), validators=[
|
||||||
DataRequired(),
|
DataRequired(),
|
||||||
NumberRange(min=1, max=500)
|
NumberRange(min=1, max=500)
|
||||||
])
|
])
|
||||||
description = TextAreaField('Description', validators=[
|
description = TextAreaField(_l('Description'), validators=[
|
||||||
Optional(),
|
Optional(),
|
||||||
Length(max=500)
|
Length(max=500)
|
||||||
])
|
])
|
||||||
is_active = BooleanField('Active', default=True)
|
is_active = BooleanField(_l('Active'), default=True)
|
||||||
submit = SubmitField('Save Classroom')
|
submit = SubmitField(_l('Save Classroom'))
|
||||||
|
|
||||||
class ClassroomResourceForm(FlaskForm):
|
class ClassroomResourceForm(FlaskForm):
|
||||||
resource_type = SelectField('Resource Type', choices=[
|
resource_type = SelectField(_l('Resource Type'), choices=[
|
||||||
('PROJECTOR', 'Projector'),
|
('PROJECTOR', _l('Projector')),
|
||||||
('WHITEBOARD', 'Whiteboard'),
|
('WHITEBOARD', _l('Whiteboard')),
|
||||||
('COMPUTER', 'Computer'),
|
('COMPUTER', _l('Computer')),
|
||||||
('SOUND_SYSTEM', 'Sound System'),
|
('SOUND_SYSTEM', _l('Sound System')),
|
||||||
('MICROPHONE', 'Microphone'),
|
('MICROPHONE', _l('Microphone')),
|
||||||
('AIR_CONDITIONER', 'Air Conditioner'),
|
('AIR_CONDITIONER', _l('Air Conditioner')),
|
||||||
('OTHER', 'Other')
|
('OTHER', _l('Other'))
|
||||||
], validators=[DataRequired()])
|
], validators=[DataRequired()])
|
||||||
brand = StringField('Brand', validators=[Optional(), Length(max=100)])
|
brand = StringField(_l('Brand'), validators=[Optional(), Length(max=100)])
|
||||||
model = StringField('Model', validators=[Optional(), Length(max=100)])
|
model = StringField(_l('Model'), validators=[Optional(), Length(max=100)])
|
||||||
specifications = TextAreaField('Specifications', validators=[Optional()])
|
specifications = TextAreaField(_l('Specifications'), validators=[Optional()])
|
||||||
functional = BooleanField('Functional', default=True)
|
functional = BooleanField(_l('Functional'), default=True)
|
||||||
submit = SubmitField('Save Resource')
|
submit = SubmitField(_l('Save Resource'))
|
||||||
@@ -1,26 +1,27 @@
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import SelectField, StringField, TextAreaField, IntegerField, SubmitField, DateTimeLocalField
|
from wtforms import SelectField, StringField, TextAreaField, IntegerField, SubmitField, DateTimeLocalField
|
||||||
from wtforms.validators import DataRequired, Length, NumberRange, Optional, ValidationError
|
from wtforms.validators import DataRequired, Length, NumberRange, Optional, ValidationError
|
||||||
|
from flask_babel import lazy_gettext as _l
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
class ReservationForm(FlaskForm):
|
class ReservationForm(FlaskForm):
|
||||||
classroom_id = SelectField('Classroom', coerce=int, validators=[DataRequired()])
|
classroom_id = SelectField(_l('Classroom'), coerce=int, validators=[DataRequired()])
|
||||||
commission_id = SelectField('Commission/Subject', coerce=int, validators=[DataRequired()])
|
commission_id = SelectField(_l('Commission/Subject'), coerce=int, validators=[DataRequired()])
|
||||||
start_time = DateTimeLocalField('Start Time', format='%Y-%m-%dT%H:%M', validators=[DataRequired()])
|
start_time = DateTimeLocalField(_l('Start Time'), format='%Y-%m-%dT%H:%M', validators=[DataRequired()])
|
||||||
end_time = DateTimeLocalField('End Time', format='%Y-%m-%dT%H:%M', validators=[DataRequired()])
|
end_time = DateTimeLocalField(_l('End Time'), format='%Y-%m-%dT%H:%M', validators=[DataRequired()])
|
||||||
purpose = StringField('Purpose', validators=[
|
purpose = StringField(_l('Purpose'), validators=[
|
||||||
DataRequired(),
|
DataRequired(),
|
||||||
Length(min=3, max=500)
|
Length(min=3, max=500)
|
||||||
])
|
])
|
||||||
expected_attendees = IntegerField('Expected Attendees', validators=[
|
expected_attendees = IntegerField(_l('Expected Attendees'), validators=[
|
||||||
DataRequired(),
|
DataRequired(),
|
||||||
NumberRange(min=1, max=500)
|
NumberRange(min=1, max=500)
|
||||||
])
|
])
|
||||||
notes = TextAreaField('Notes', validators=[
|
notes = TextAreaField(_l('Notes'), validators=[
|
||||||
Optional(),
|
Optional(),
|
||||||
Length(max=1000)
|
Length(max=1000)
|
||||||
])
|
])
|
||||||
submit = SubmitField('Make Reservation')
|
submit = SubmitField(_l('Make Reservation'))
|
||||||
|
|
||||||
def validate_end_time(self, field):
|
def validate_end_time(self, field):
|
||||||
if field.data <= self.start_time.data:
|
if field.data <= self.start_time.data:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class User(UserMixin, db.Model):
|
|||||||
is_active = db.Column(db.Boolean, default=True, nullable=False)
|
is_active = db.Column(db.Boolean, default=True, nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
last_login = db.Column(db.DateTime)
|
last_login = db.Column(db.DateTime)
|
||||||
|
preferred_language = db.Column(db.String(10), default=None, nullable=True)
|
||||||
|
|
||||||
# Virtual fields for future compatibility
|
# Virtual fields for future compatibility
|
||||||
@property
|
@property
|
||||||
|
|||||||
+16
-1
@@ -25,6 +25,10 @@ def login():
|
|||||||
login_user(user, remember=form.remember_me.data)
|
login_user(user, remember=form.remember_me.data)
|
||||||
user.update_last_login()
|
user.update_last_login()
|
||||||
|
|
||||||
|
# Si el usuario tiene idioma configurado en su perfil, sobrescribe el navegador en toda la sesión
|
||||||
|
if user.preferred_language and user.preferred_language in ['es', 'en']:
|
||||||
|
session['language'] = user.preferred_language
|
||||||
|
|
||||||
next_page = request.args.get('next')
|
next_page = request.args.get('next')
|
||||||
if not next_page or not next_page.startswith('/'):
|
if not next_page or not next_page.startswith('/'):
|
||||||
next_page = url_for('main.dashboard')
|
next_page = url_for('main.dashboard')
|
||||||
@@ -41,15 +45,26 @@ def login():
|
|||||||
def logout():
|
def logout():
|
||||||
"""Handle user logout"""
|
"""Handle user logout"""
|
||||||
logout_user()
|
logout_user()
|
||||||
|
# Al cerrar sesión, se limpia el idioma para que el login use el del navegador por defecto
|
||||||
|
session.pop('language', None)
|
||||||
flash('You have been logged out successfully.', 'info')
|
flash('You have been logged out successfully.', 'info')
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
@auth_bp.route('/profile')
|
@auth_bp.route('/profile', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def profile():
|
def profile():
|
||||||
"""User profile page"""
|
"""User profile page"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
lang = request.form.get('preferred_language')
|
||||||
|
if lang in ['es', 'en']:
|
||||||
|
current_user.preferred_language = lang
|
||||||
|
session['language'] = lang
|
||||||
|
db.session.commit()
|
||||||
|
flash('Idioma de preferencia actualizado correctamente.', 'success')
|
||||||
|
return redirect(url_for('auth.profile'))
|
||||||
return render_template('auth/profile.html')
|
return render_template('auth/profile.html')
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/change_password', methods=['GET', 'POST'])
|
@auth_bp.route('/change_password', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def change_password():
|
def change_password():
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ def add_classroom():
|
|||||||
|
|
||||||
return render_template('classrooms/add.html', form=form, title='Add Classroom')
|
return render_template('classrooms/add.html', form=form, title='Add Classroom')
|
||||||
|
|
||||||
|
@classrooms_bp.route('/', endpoint='index')
|
||||||
@classrooms_bp.route('/list')
|
@classrooms_bp.route('/list')
|
||||||
@login_required
|
@login_required
|
||||||
def list_classrooms():
|
def list_classrooms():
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from flask import Blueprint, render_template, jsonify, redirect, url_for, session, request, flash
|
from flask import Blueprint, render_template, jsonify, redirect, url_for, session, request, flash
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
|
from app import db
|
||||||
from app.models.reservation import Reservation
|
from app.models.reservation import Reservation
|
||||||
from app.models.classroom import Classroom
|
from app.models.classroom import Classroom
|
||||||
from app.models.subject import Subject
|
from app.models.subject import Subject
|
||||||
@@ -124,4 +125,8 @@ def set_language(language=None):
|
|||||||
language = 'en'
|
language = 'en'
|
||||||
|
|
||||||
session['language'] = language
|
session['language'] = language
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
current_user.preferred_language = language
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
return redirect(request.referrer or url_for('main.dashboard'))
|
return redirect(request.referrer or url_for('main.dashboard'))
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Acceso Denegado (403) - Edu-Space Admin{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container pt-5 mt-5 text-center">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card border-0 shadow-sm p-5">
|
||||||
|
<div class="text-danger mb-3" style="font-size: 4rem;">
|
||||||
|
<i class="bi bi-shield-x"></i>
|
||||||
|
</div>
|
||||||
|
<h1 class="h2 mb-2">403 - Acceso Denegado</h1>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
No tienes los permisos necesarios para acceder a este recurso. Esta sección está restringida exclusivamente a administradores.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('main.dashboard') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-house me-1"></i>Ir al Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Cambiar Contraseña - Edu-Space Admin{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container pt-5 mt-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-6 col-md-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0">
|
||||||
|
<i class="bi bi-shield-lock text-primary me-2"></i>Cambiar Contraseña
|
||||||
|
</h1>
|
||||||
|
<a href="{{ url_for('auth.profile') }}" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="bi bi-arrow-left me-1"></i>Volver al Perfil
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Card -->
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<form method="POST" action="{{ url_for('auth.change_password') }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="current_password" class="form-label">Contraseña Actual</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="bi bi-lock"></i></span>
|
||||||
|
<input type="password" class="form-control" id="current_password" name="current_password" required placeholder="Ingresa tu contraseña actual">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new_password" class="form-label">Nueva Contraseña</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||||
|
<input type="password" class="form-control" id="new_password" name="new_password" required minlength="6" placeholder="Mínimo 6 caracteres">
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Debe tener al menos 6 caracteres.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="confirm_password" class="form-label">Confirmar Nueva Contraseña</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="bi bi-check2-circle"></i></span>
|
||||||
|
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required minlength="6" placeholder="Repite la nueva contraseña">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-save me-1"></i>Actualizar Contraseña
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('auth.profile') }}" class="btn btn-link text-muted">
|
||||||
|
Cancelar
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,42 +1,54 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="{{ session.language or request.accept_languages.best_match(['es', 'en']) or 'es' }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Login - Edu-Space Admin</title>
|
<title>{% trans %}Login - Edu-Space Admin{% endtrans %}</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
}
|
}
|
||||||
.login-card {
|
.login-card {
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(30, 41, 59, 0.95);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5);
|
||||||
max-width: 450px;
|
max-width: 450px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
color: #f8fafc;
|
||||||
}
|
}
|
||||||
.logo-section {
|
.logo-section {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||||
border-radius: 20px 20px 0 0;
|
border-radius: 20px 20px 0 0;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.lang-switch {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<div class="logo-section">
|
<div class="logo-section">
|
||||||
|
<div class="lang-switch">
|
||||||
|
{% include 'partials/language_switcher.html' %}
|
||||||
|
</div>
|
||||||
<i class="bi bi-building" style="font-size: 3rem;"></i>
|
<i class="bi bi-building" style="font-size: 3rem;"></i>
|
||||||
<h3 class="mt-3">Edu-Space Admin</h3>
|
<h3 class="mt-3 mb-1 fw-bold">Edu-Space Admin</h3>
|
||||||
<p class="mb-0">Classroom Management System</p>
|
<p class="mb-0 text-white-50 small">{% trans %}Classroom Management System{% endtrans %}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
@@ -55,10 +67,10 @@
|
|||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="email" class="form-label">{{ form.email.label.text }}</label>
|
<label for="email" class="form-label small text-secondary">{{ form.email.label.text }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
|
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-envelope"></i></span>
|
||||||
{{ form.email(class="form-control", placeholder="Enter your email") }}
|
<input type="email" class="form-control bg-dark border-secondary text-white" id="email" name="email" value="{{ form.email.data or '' }}" required placeholder="{% trans %}Enter your email{% endtrans %}">
|
||||||
</div>
|
</div>
|
||||||
{% for error in form.email.errors %}
|
{% for error in form.email.errors %}
|
||||||
<div class="text-danger small mt-1">{{ error }}</div>
|
<div class="text-danger small mt-1">{{ error }}</div>
|
||||||
@@ -66,10 +78,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">{{ form.password.label.text }}</label>
|
<label for="password" class="form-label small text-secondary">{{ form.password.label.text }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text"><i class="bi bi-lock"></i></span>
|
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-lock"></i></span>
|
||||||
{{ form.password(class="form-control", placeholder="Enter your password") }}
|
<input type="password" class="form-control bg-dark border-secondary text-white" id="password" name="password" required placeholder="{% trans %}Enter your password{% endtrans %}">
|
||||||
</div>
|
</div>
|
||||||
{% for error in form.password.errors %}
|
{% for error in form.password.errors %}
|
||||||
<div class="text-danger small mt-1">{{ error }}</div>
|
<div class="text-danger small mt-1">{{ error }}</div>
|
||||||
@@ -78,18 +90,20 @@
|
|||||||
|
|
||||||
<div class="mb-3 form-check">
|
<div class="mb-3 form-check">
|
||||||
{{ form.remember_me(class="form-check-input") }}
|
{{ form.remember_me(class="form-check-input") }}
|
||||||
{{ form.remember_me.label(class="form-check-label") }}
|
<label class="form-check-label text-secondary small" for="remember_me">{{ form.remember_me.label.text }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
{{ form.submit(class="btn btn-primary btn-lg") }}
|
<button type="submit" class="btn btn-primary btn-lg fw-semibold">
|
||||||
|
<i class="bi bi-box-arrow-in-right me-1"></i> {{ form.submit.label.text }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="text-center mt-3">
|
<div class="text-center mt-4">
|
||||||
<small class="text-muted">
|
<small class="text-secondary">
|
||||||
<i class="bi bi-shield-lock"></i>
|
<i class="bi bi-shield-lock me-1"></i>
|
||||||
Secure Login • All access is logged
|
{% trans %}Secure Login • All access is logged{% endtrans %}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{% trans %}User Profile{% endtrans %} - Edu-Space Admin{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container pt-5 mt-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0">
|
||||||
|
<i class="bi bi-person-badge text-primary me-2"></i>{% trans %}My Profile{% endtrans %}
|
||||||
|
</h1>
|
||||||
|
<a href="{{ url_for('main.dashboard') }}" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="bi bi-arrow-left me-1"></i>{% trans %}Dashboard{% endtrans %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profile Card -->
|
||||||
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="d-flex align-items-center mb-4">
|
||||||
|
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 72px; height: 72px; font-size: 2rem;">
|
||||||
|
<i class="bi bi-person"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-1">{{ current_user.name }}</h4>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="badge bg-primary">{{ current_user.role }}</span>
|
||||||
|
{% if current_user.is_active %}
|
||||||
|
<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>{% trans %}Active{% endtrans %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger"><i class="bi bi-x-circle me-1"></i>{% trans %}Inactive{% endtrans %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<!-- User Details Grid -->
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<label class="form-label text-muted small mb-1">{% trans %}Email{% endtrans %}</label>
|
||||||
|
<div class="fw-semibold">{{ current_user.email }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<label class="form-label text-muted small mb-1">{% trans %}Full Name{% endtrans %}</label>
|
||||||
|
<div class="fw-semibold">{{ current_user.name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<label class="form-label text-muted small mb-1">{% trans %}Access Role{% endtrans %}</label>
|
||||||
|
<div class="fw-semibold">{{ current_user.role }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<label class="form-label text-muted small mb-1">{% trans %}Last Login{% endtrans %}</label>
|
||||||
|
<div class="fw-semibold">
|
||||||
|
{% if current_user.last_login %}
|
||||||
|
{{ current_user.last_login.strftime('%Y-%m-%d %H:%M:%S') }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">{% trans %}First login{% endtrans %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<label class="form-label text-muted small mb-1">{% trans %}Registration Date{% endtrans %}</label>
|
||||||
|
<div class="fw-semibold">
|
||||||
|
{% if current_user.created_at %}
|
||||||
|
{{ current_user.created_at.strftime('%Y-%m-%d') }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<!-- Language Preference Section -->
|
||||||
|
<form method="POST" action="{{ url_for('auth.profile') }}">
|
||||||
|
<label class="form-label fw-bold mb-2">
|
||||||
|
<i class="bi bi-translate text-primary me-2"></i>{% trans %}Language Preference{% endtrans %}
|
||||||
|
</label>
|
||||||
|
<p class="text-muted small mb-3">
|
||||||
|
{% trans %}If set, this language overrides the browser configuration for all your sessions.{% endtrans %}
|
||||||
|
</p>
|
||||||
|
<div class="row g-2 align-items-center">
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<select class="form-select" name="preferred_language">
|
||||||
|
<option value="" {% if not current_user.preferred_language %}selected{% endif %}>
|
||||||
|
{% trans %}Auto-detect from browser (Default){% endtrans %}
|
||||||
|
</option>
|
||||||
|
<option value="es" {% if current_user.preferred_language == 'es' %}selected{% endif %}>
|
||||||
|
Español
|
||||||
|
</option>
|
||||||
|
<option value="en" {% if current_user.preferred_language == 'en' %}selected{% endif %}>
|
||||||
|
English
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<button type="submit" class="btn btn-outline-primary w-100">
|
||||||
|
<i class="bi bi-check2 me-1"></i>{% trans %}Save Preference{% endtrans %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-body-tertiary px-4 py-3 d-flex justify-content-between align-items-center">
|
||||||
|
<span class="text-muted small">{% trans %}Account Security{% endtrans %}</span>
|
||||||
|
<a href="{{ url_for('auth.change_password') }}" class="btn btn-primary btn-sm">
|
||||||
|
<i class="bi bi-key me-1"></i>{% trans %}Change Password{% endtrans %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Reservations Overview (if any) -->
|
||||||
|
{% if current_user.reservations %}
|
||||||
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-header bg-body-tertiary">
|
||||||
|
<h5 class="card-title mb-0 h6">
|
||||||
|
<i class="bi bi-calendar-event me-2"></i>{% trans %}My Recent Reservations{% endtrans %}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>{% trans %}Classroom{% endtrans %}</th>
|
||||||
|
<th>{% trans %}Purpose{% endtrans %}</th>
|
||||||
|
<th>{% trans %}Date & Time{% endtrans %}</th>
|
||||||
|
<th>{% trans %}Status{% endtrans %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for res in current_user.reservations[:5] %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-secondary">{{ res.classroom.code_display if res.classroom else 'N/A' }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ res.purpose }}</td>
|
||||||
|
<td>{{ res.start_time.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{{ 'success' if res.status == 'CONFIRMED' else 'warning' if res.status == 'PENDING' else 'secondary' }}">
|
||||||
|
{{ res.status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
+12
-31
@@ -31,62 +31,43 @@
|
|||||||
<ul class="navbar-nav me-auto">
|
<ul class="navbar-nav me-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('main.dashboard') }}">
|
<a class="nav-link" href="{{ url_for('main.dashboard') }}">
|
||||||
<i class="bi bi-speedometer2"></i> Dashboard
|
<i class="bi bi-speedometer2"></i> {% trans %}Dashboard{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||||
<i class="bi bi-door-open"></i> Classrooms
|
<i class="bi bi-door-open"></i> {% trans %}Classrooms{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item" href="{{ url_for('classrooms.list_classrooms') }}">
|
<li><a class="dropdown-item" href="{{ url_for('classrooms.list_classrooms') }}">
|
||||||
<i class="bi bi-list"></i> All Classrooms
|
<i class="bi bi-list"></i> {% trans %}All Classrooms{% endtrans %}
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('classrooms.add_classroom') }}">
|
<li><a class="dropdown-item" href="{{ url_for('classrooms.add_classroom') }}">
|
||||||
<i class="bi bi-plus-circle"></i> Add Classroom
|
<i class="bi bi-plus-circle"></i> {% trans %}Add Classroom{% endtrans %}
|
||||||
</a></li>
|
</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||||
<i class="bi bi-calendar3"></i> Schedule
|
<i class="bi bi-calendar3"></i> {% trans %}Schedule{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item" href="{{ url_for('schedule.calendar_view') }}">
|
<li><a class="dropdown-item" href="{{ url_for('schedule.calendar_view') }}">
|
||||||
<i class="bi bi-calendar-week"></i> Calendar View
|
<i class="bi bi-calendar-week"></i> {% trans %}Calendar View{% endtrans %}
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('schedule.list_reservations') }}">
|
<li><a class="dropdown-item" href="{{ url_for('schedule.list_reservations') }}">
|
||||||
<i class="bi bi-list-check"></i> All Reservations
|
<i class="bi bi-list-check"></i> {% trans %}All Reservations{% endtrans %}
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('schedule.add_reservation') }}">
|
<li><a class="dropdown-item" href="{{ url_for('schedule.add_reservation') }}">
|
||||||
<i class="bi bi-plus-circle"></i> New Reservation
|
<i class="bi bi-plus-circle"></i> {% trans %}New Reservation{% endtrans %}
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('schedule.today_schedule') }}">
|
<li><a class="dropdown-item" href="{{ url_for('schedule.today_schedule') }}">
|
||||||
<i class="bi bi-calendar-today"></i> Today's Schedule
|
<i class="bi bi-calendar-today"></i> {% trans %}Today's Schedule{% endtrans %}
|
||||||
</a></li>
|
</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
<li class="nav-item dropdown">
|
|
||||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
|
||||||
<i class="bi bi-person-circle"></i> {{ current_user.first_name }} {{ current_user.last_name }}
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('auth.profile') }}">
|
|
||||||
<i class="bi bi-person"></i> Profile
|
|
||||||
</a></li>
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('auth.change_password') }}">
|
|
||||||
<i class="bi bi-key"></i> Change Password
|
|
||||||
</a></li>
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">
|
|
||||||
<i class="bi bi-box-arrow-right"></i> Logout
|
|
||||||
</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<!-- Language Switcher -->
|
<!-- Language Switcher -->
|
||||||
@@ -100,14 +81,14 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li><a class="dropdown-item" href="{{ url_for('auth.profile') }}">
|
<li><a class="dropdown-item" href="{{ url_for('auth.profile') }}">
|
||||||
<i class="bi bi-person"></i> Profile
|
<i class="bi bi-person"></i> {% trans %}Profile{% endtrans %}
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('auth.change_password') }}">
|
<li><a class="dropdown-item" href="{{ url_for('auth.change_password') }}">
|
||||||
<i class="bi bi-key"></i> Change Password
|
<i class="bi bi-key"></i> {% trans %}Change Password{% endtrans %}
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">
|
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">
|
||||||
<i class="bi bi-box-arrow-right"></i> Logout
|
<i class="bi bi-box-arrow-right"></i> {% trans %}Logout{% endtrans %}
|
||||||
</a></li>
|
</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Add Classroom - Edu-Space Admin{% endblock %}
|
{% block title %}{% trans %}Add Classroom{% endtrans %} - Edu-Space Admin{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid pt-5 mt-4">
|
<div class="container-fluid pt-5 mt-4">
|
||||||
@@ -8,10 +8,10 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h1 class="h3">
|
<h1 class="h3">
|
||||||
<i class="bi bi-plus-circle text-primary"></i> Add Classroom
|
<i class="bi bi-plus-circle text-primary"></i> {% trans %}Add Classroom{% endtrans %}
|
||||||
</h1>
|
</h1>
|
||||||
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-outline-secondary">
|
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-outline-secondary">
|
||||||
<i class="bi bi-arrow-left"></i> Back to Classrooms
|
<i class="bi bi-arrow-left"></i> {% trans %}Back to Classrooms{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,9 +20,9 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-8 mx-auto">
|
<div class="col-lg-8 mx-auto">
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-header bg-white border-0">
|
<div class="card-header bg-body-tertiary border-0 py-3">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0 h6 fw-bold">
|
||||||
<i class="bi bi-door-open text-primary"></i> Classroom Information
|
<i class="bi bi-door-open text-primary me-2"></i> {% trans %}Classroom Information{% endtrans %}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -73,16 +73,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
{{ form.is_active.label(class="form-check-label") }}
|
<label class="form-label d-block"> </label>
|
||||||
<div class="form-check">
|
<div class="form-check pt-2">
|
||||||
{{ form.is_active(class="form-check-input") }}
|
{{ form.is_active(class="form-check-input") }}
|
||||||
|
{{ form.is_active.label(class="form-check-label") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
{{ form.description.label(class="form-label") }}
|
{{ form.description.label(class="form-label") }}
|
||||||
{{ form.description(class="form-control", rows="3", placeholder="Optional: Add any additional details about this classroom...") }}
|
{{ form.description(class="form-control", rows="3", placeholder=_("Optional: Add any additional details about this classroom...")) }}
|
||||||
{% if form.description.errors %}
|
{% if form.description.errors %}
|
||||||
<div class="text-danger small mt-1">
|
<div class="text-danger small mt-1">
|
||||||
{{ form.description.errors[0] }}
|
{{ form.description.errors[0] }}
|
||||||
@@ -93,13 +94,13 @@
|
|||||||
<!-- Preview Section -->
|
<!-- Preview Section -->
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<h6 class="alert-heading">
|
<h6 class="alert-heading">
|
||||||
<i class="bi bi-info-circle"></i> Preview
|
<i class="bi bi-info-circle"></i> {% trans %}Preview{% endtrans %}
|
||||||
</h6>
|
</h6>
|
||||||
<div id="classroom-preview">
|
<div id="classroom-preview">
|
||||||
<p class="mb-0">Fill in the form to see a preview of the classroom code:
|
<p class="mb-1">{% trans %}Fill in the form to see a preview of the classroom code:{% endtrans %}
|
||||||
<span class="fw-bold" id="preview-code">-</span>
|
<span class="fw-bold" id="preview-code">-</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-0">Full location:
|
<p class="mb-0">{% trans %}Full location:{% endtrans %}
|
||||||
<span class="fw-bold" id="preview-location">-</span>
|
<span class="fw-bold" id="preview-location">-</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,9 +108,11 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-outline-secondary">
|
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-outline-secondary">
|
||||||
<i class="bi bi-x-circle"></i> Cancel
|
<i class="bi bi-x-circle"></i> {% trans %}Cancel{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
{{ form.submit(class="btn btn-primary") }}
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-save me-1"></i> {{ form.submit.label.text }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,16 +121,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Resources Section -->
|
<!-- Resources Section -->
|
||||||
<div class="row mt-4">
|
<div class="row mt-4 mb-4">
|
||||||
<div class="col-lg-8 mx-auto">
|
<div class="col-lg-8 mx-auto">
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-header bg-white border-0">
|
<div class="card-header bg-body-tertiary border-0 py-3">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0 h6 fw-bold">
|
||||||
<i class="bi bi-tools text-warning"></i> Resources (Optional)
|
<i class="bi bi-tools text-warning me-2"></i> {% trans %}Resources (Optional){% endtrans %}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-muted">You can add classroom resources after creating the classroom.</p>
|
<p class="text-muted mb-0">{% trans %}You can add classroom resources after creating the classroom.{% endtrans %}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,10 +145,11 @@ $(document).ready(function() {
|
|||||||
const building = $('#building').val();
|
const building = $('#building').val();
|
||||||
const roomNumber = $('#room_number').val();
|
const roomNumber = $('#room_number').val();
|
||||||
const floor = $('#floor').val();
|
const floor = $('#floor').val();
|
||||||
|
const floorLabel = '{{ _("Floor") }}';
|
||||||
|
|
||||||
if (building && roomNumber && floor) {
|
if (building && roomNumber && floor) {
|
||||||
$('#preview-code').text(`${building}-${roomNumber}`);
|
$('#preview-code').text(`${building}-${roomNumber}`);
|
||||||
$('#preview-location').text(`${building} - Floor ${floor}`);
|
$('#preview-location').text(`${building} - ${floorLabel} ${floor}`);
|
||||||
} else {
|
} else {
|
||||||
$('#preview-code').text('-');
|
$('#preview-code').text('-');
|
||||||
$('#preview-location').text('-');
|
$('#preview-location').text('-');
|
||||||
@@ -162,7 +166,6 @@ $(document).ready(function() {
|
|||||||
$('#classroom-form').on('submit', function(e) {
|
$('#classroom-form').on('submit', function(e) {
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
|
||||||
// Required fields validation
|
|
||||||
if (!$('#building').val().trim()) {
|
if (!$('#building').val().trim()) {
|
||||||
$('#building').addClass('is-invalid');
|
$('#building').addClass('is-invalid');
|
||||||
isValid = false;
|
isValid = false;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Classrooms - Edu-Space Admin{% endblock %}
|
{% block title %}{% trans %}Classrooms{% endtrans %} - Edu-Space Admin{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid pt-5 mt-4">
|
<div class="container-fluid pt-5 mt-4">
|
||||||
@@ -8,11 +8,11 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h1 class="h3">
|
<h1 class="h3">
|
||||||
<i class="bi bi-door-open text-primary"></i> Classrooms
|
<i class="bi bi-door-open text-primary"></i> {% trans %}Classrooms{% endtrans %}
|
||||||
</h1>
|
</h1>
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ url_for('classrooms.add_classroom') }}" class="btn btn-primary">
|
<a href="{{ url_for('classrooms.add_classroom') }}" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-circle"></i> Add Classroom
|
<i class="bi bi-plus-circle"></i> {% trans %}Add Classroom{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,24 +27,24 @@
|
|||||||
<form method="GET" class="row g-3">
|
<form method="GET" class="row g-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<input type="text" name="search" class="form-control"
|
<input type="text" name="search" class="form-control"
|
||||||
placeholder="Search by building, room, description..."
|
placeholder="{% trans %}Search by building, room, description...{% endtrans %}"
|
||||||
value="{{ search }}">
|
value="{{ search }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="form-check">
|
<div class="form-check pt-2">
|
||||||
<input class="form-check-input" type="checkbox" name="active_only"
|
<input class="form-check-input" type="checkbox" name="active_only"
|
||||||
id="activeOnly" {% if active_only %}checked{% endif %}>
|
id="activeOnly" {% if active_only %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="activeOnly">
|
<label class="form-check-label" for="activeOnly">
|
||||||
Show active classrooms only
|
{% trans %}Show active classrooms only{% endtrans %}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<button type="submit" class="btn btn-outline-primary">
|
<button type="submit" class="btn btn-outline-primary">
|
||||||
<i class="bi bi-search"></i> Search
|
<i class="bi bi-search"></i> {% trans %}Search{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-outline-secondary">
|
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-outline-secondary">
|
||||||
<i class="bi bi-x-circle"></i> Clear
|
<i class="bi bi-x-circle"></i> {% trans %}Clear{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -63,24 +63,24 @@
|
|||||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h5 class="card-title mb-1">
|
<h5 class="card-title mb-1">
|
||||||
{{ classroom.code }}
|
{{ classroom.code_display }}
|
||||||
</h5>
|
</h5>
|
||||||
<p class="text-muted small mb-2">
|
<p class="text-muted small mb-2">
|
||||||
<i class="bi bi-geo-alt"></i> {{ classroom.get_full_location() }}
|
<i class="bi bi-geo-alt"></i> {{ classroom.get_full_location() }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge {% if classroom.is_active %}bg-success{% else %}bg-secondary{% endif %}">
|
<span class="badge {% if classroom.is_active %}bg-success{% else %}bg-secondary{% endif %}">
|
||||||
{% if classroom.is_active %}Active{% else %}Inactive{% endif %}
|
{% if classroom.is_active %}{% trans %}Active{% endtrans %}{% else %}{% trans %}Inactive{% endtrans %}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row small">
|
<div class="row small">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<span class="text-muted">Capacity:</span>
|
<span class="text-muted">{% trans %}Capacity{% endtrans %}:</span>
|
||||||
<strong>{{ classroom.capacity }}</strong>
|
<strong>{{ classroom.capacity }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<span class="text-muted">Building:</span>
|
<span class="text-muted">{% trans %}Building{% endtrans %}:</span>
|
||||||
<strong>{{ classroom.building }}</strong>
|
<strong>{{ classroom.building }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,24 +96,24 @@
|
|||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<i class="bi bi-tools"></i>
|
<i class="bi bi-tools"></i>
|
||||||
{% if classroom.resources %}
|
{% if classroom.resources %}
|
||||||
{{ classroom.resources|length }} resources
|
{{ classroom.resources|length }} {% trans %}resources{% endtrans %}
|
||||||
{% else %}
|
{% else %}
|
||||||
No resources registered
|
{% trans %}No resources registered{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer bg-white border-0">
|
<div class="card-footer bg-body-tertiary border-0">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ url_for('classrooms.view_classroom', id=classroom.id) }}"
|
<a href="{{ url_for('classrooms.view_classroom', id=classroom.id) }}"
|
||||||
class="btn btn-sm btn-outline-primary">
|
class="btn btn-sm btn-outline-primary">
|
||||||
<i class="bi bi-eye"></i> View
|
<i class="bi bi-eye"></i> {% trans %}View{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('classrooms.edit_classroom', id=classroom.id) }}"
|
<a href="{{ url_for('classrooms.edit_classroom', id=classroom.id) }}"
|
||||||
class="btn btn-sm btn-outline-secondary">
|
class="btn btn-sm btn-outline-secondary">
|
||||||
<i class="bi bi-pencil"></i> Edit
|
<i class="bi bi-pencil"></i> {% trans %}Edit{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="{{ url_for('classrooms.toggle_classroom', id=classroom.id) }}"
|
<form method="POST" action="{{ url_for('classrooms.toggle_classroom', id=classroom.id) }}"
|
||||||
@@ -121,9 +121,9 @@
|
|||||||
<button type="submit" class="btn btn-sm
|
<button type="submit" class="btn btn-sm
|
||||||
{% if classroom.is_active %}btn-outline-warning{% else %}btn-outline-success{% endif %}">
|
{% if classroom.is_active %}btn-outline-warning{% else %}btn-outline-success{% endif %}">
|
||||||
{% if classroom.is_active %}
|
{% if classroom.is_active %}
|
||||||
<i class="bi bi-pause-circle"></i> Deactivate
|
<i class="bi bi-pause-circle"></i> {% trans %}Deactivate{% endtrans %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-play-circle"></i> Activate
|
<i class="bi bi-play-circle"></i> {% trans %}Activate{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
{% if classrooms.has_prev %}
|
{% if classrooms.has_prev %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=classrooms.prev_num, search=search, active_only=active_only) }}">
|
<a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=classrooms.prev_num, search=search, active_only=active_only) }}">
|
||||||
Previous
|
{% trans %}Previous{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
{% if classrooms.has_next %}
|
{% if classrooms.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=classrooms.next_num, search=search, active_only=active_only) }}">
|
<a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=classrooms.next_num, search=search, active_only=active_only) }}">
|
||||||
Next
|
{% trans %}Next{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -182,16 +182,16 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
<i class="bi bi-door-open text-muted" style="font-size: 4rem;"></i>
|
<i class="bi bi-door-open text-muted" style="font-size: 4rem;"></i>
|
||||||
<h4 class="mt-3 text-muted">No classrooms found</h4>
|
<h4 class="mt-3 text-muted">{% trans %}No classrooms found{% endtrans %}</h4>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
{% if search %}
|
{% if search %}
|
||||||
No classrooms match your search criteria.
|
{% trans %}No classrooms match your search criteria.{% endtrans %}
|
||||||
{% else %}
|
{% else %}
|
||||||
No classrooms have been registered yet.
|
{% trans %}No classrooms have been registered yet.{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<a href="{{ url_for('classrooms.add_classroom') }}" class="btn btn-primary">
|
<a href="{{ url_for('classrooms.add_classroom') }}" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-circle"></i> Add First Classroom
|
<i class="bi bi-plus-circle"></i> {% trans %}Add First Classroom{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-header bg-white border-0">
|
<div class="card-header bg-white border-0">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="bi bi-graph-up text-primary"></i> Monthly Reservations
|
<i class="bi bi-graph-up text-primary"></i> {% trans %}Monthly Reservations{% endtrans %}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -102,28 +102,28 @@
|
|||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-header bg-white border-0">
|
<div class="card-header bg-white border-0">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="bi bi-lightning text-warning"></i> Quick Actions
|
<i class="bi bi-lightning text-warning"></i> {% trans %}Quick Actions{% endtrans %}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<a href="{{ url_for('classrooms.add_classroom') }}" class="btn btn-outline-primary">
|
<a href="{{ url_for('classrooms.add_classroom') }}" class="btn btn-outline-primary">
|
||||||
<i class="bi bi-plus-circle"></i> Add Classroom
|
<i class="bi bi-plus-circle"></i> {% trans %}Add Classroom{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('schedule.commissions_view') }}" class="btn btn-outline-warning">
|
<a href="{{ url_for('schedule.commissions_view') }}" class="btn btn-outline-warning">
|
||||||
<i class="bi bi-people"></i> Manage Commissions
|
<i class="bi bi-people"></i> {% trans %}Manage Commissions{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-outline-success">
|
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-outline-success">
|
||||||
<i class="bi bi-calendar-plus"></i> New Reservation
|
<i class="bi bi-calendar-plus"></i> {% trans %}New Reservation{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('schedule.today_schedule') }}" class="btn btn-outline-info">
|
<a href="{{ url_for('schedule.today_schedule') }}" class="btn btn-outline-info">
|
||||||
<i class="bi bi-calendar-today"></i> Today's Schedule
|
<i class="bi bi-calendar-today"></i> {% trans %}Today's Schedule{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('genetic_algorithm_web.genetic_optimizer') }}" class="btn btn-info text-white">
|
<a href="{{ url_for('genetic_algorithm_web.genetic_optimizer') }}" class="btn btn-info text-white">
|
||||||
<i class="bi bi-cpu"></i> AI Room Optimizer
|
<i class="bi bi-cpu"></i> {% trans %}AI Room Optimizer{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('schedule.calendar_view') }}" class="btn btn-outline-secondary">
|
<a href="{{ url_for('schedule.calendar_view') }}" class="btn btn-outline-secondary">
|
||||||
<i class="bi bi-calendar3"></i> View Calendar
|
<i class="bi bi-calendar3"></i> {% trans %}View Calendar{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,10 +138,10 @@
|
|||||||
<div class="card-header bg-white border-0">
|
<div class="card-header bg-white border-0">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="bi bi-clock-history text-info"></i> Upcoming Reservations (Next 7 Days)
|
<i class="bi bi-clock-history text-info"></i> {% trans %}Upcoming Reservations (Next 7 Days){% endtrans %}
|
||||||
</h5>
|
</h5>
|
||||||
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-sm btn-outline-primary">
|
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-sm btn-outline-primary">
|
||||||
View All
|
{% trans %}View All{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,19 +151,19 @@
|
|||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date & Time</th>
|
<th>{% trans %}Date & Time{% endtrans %}</th>
|
||||||
<th>Classroom</th>
|
<th>{% trans %}Classroom{% endtrans %}</th>
|
||||||
<th>Subject</th>
|
<th>{% trans %}Subject{% endtrans %}</th>
|
||||||
<th>Teacher</th>
|
<th>{% trans %}Teacher{% endtrans %}</th>
|
||||||
<th>Status</th>
|
<th>{% trans %}Status{% endtrans %}</th>
|
||||||
<th>Actions</th>
|
<th>{% trans %}Actions{% endtrans %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for reservation in upcoming_reservations %}
|
{% for reservation in upcoming_reservations %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ reservation.start_time.strftime('%Y-%m-%d %H:%M') }}</td>
|
<td>{{ reservation.start_time.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||||
<td>{{ reservation.classroom.code }}</td>
|
<td>{{ reservation.classroom.code }}</td>
|
||||||
<td>{{ reservation.commission.get_full_code() }}</td>
|
<td>{{ reservation.commission.get_full_code() }}</td>
|
||||||
<td>{{ reservation.commission.teacher_name }}</td>
|
<td>{{ reservation.commission.teacher_name }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for('schedule.list_reservations') }}"
|
<a href="{{ url_for('schedule.list_reservations') }}"
|
||||||
class="btn btn-sm btn-outline-primary">
|
class="btn btn-sm btn-outline-primary">
|
||||||
<i class="bi bi-eye"></i> View
|
<i class="bi bi-eye"></i> {% trans %}View{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-4">
|
<div class="text-center py-4">
|
||||||
<i class="bi bi-calendar-x text-muted" style="font-size: 3rem;"></i>
|
<i class="bi bi-calendar-x text-muted" style="font-size: 3rem;"></i>
|
||||||
<p class="text-muted mt-3">No upcoming reservations in the next 7 days</p>
|
<p class="text-muted mt-3">{% trans %}No upcoming reservations in the next 7 days{% endtrans %}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}AI Room Reservation Optimizer{% endblock %}
|
{% block title %}{% trans %}AI Room Reservation Optimizer{% endtrans %} - Edu-Space Admin{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<style>
|
<style>
|
||||||
@@ -13,27 +13,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.commission-item {
|
.commission-item {
|
||||||
border-left: 4px solid #007bff;
|
border-left: 4px solid var(--bs-primary);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.commission-item:hover {
|
.commission-item:hover {
|
||||||
border-left-color: #0056b3;
|
border-left-color: var(--bs-info);
|
||||||
background-color: #f8f9fa;
|
background-color: rgba(var(--bs-primary-rgb), 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.commission-item.selected {
|
.commission-item.selected {
|
||||||
border-left-color: #28a745;
|
border-left-color: var(--bs-success);
|
||||||
background-color: #d4edda;
|
background-color: rgba(var(--bs-success-rgb), 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.algorithm-status {
|
.algorithm-status {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 20px;
|
top: 90px;
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner-overlay {
|
.spinner-overlay {
|
||||||
@@ -42,99 +39,93 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
display: none;
|
display: none;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fitness-meter {
|
.hero-banner {
|
||||||
height: 20px;
|
background: linear-gradient(135deg, rgba(37, 99, 235, 0.2) 0%, rgba(30, 41, 59, 0.9) 100%);
|
||||||
background: linear-gradient(to right, #dc3545, #ffc107, #28a745);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
border-radius: 10px;
|
border-radius: 12px;
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fitness-indicator {
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
background: white;
|
|
||||||
border: 3px solid #007bff;
|
|
||||||
border-radius: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
transition: left 0.5s ease;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid pt-5 mt-4">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card bg-primary text-white">
|
<div class="card hero-banner border-0 shadow-sm p-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h1 class="card-title mb-3">
|
<div class="d-flex align-items-center mb-2">
|
||||||
<i class="fas fa-brain me-2"></i>
|
<div class="bg-primary bg-opacity-25 rounded-3 p-3 me-3 text-primary">
|
||||||
AI Room Reservation Optimizer
|
<i class="bi bi-cpu fs-2"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-1 fw-bold">
|
||||||
|
{% trans %}AI Room Reservation Optimizer{% endtrans %}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="card-text mb-0">
|
<p class="text-muted mb-0">
|
||||||
Use genetic algorithms to automatically optimize room assignments based on capacity, availability, and student enrollment.
|
{% trans %}Use genetic algorithms to automatically optimize room assignments based on capacity, availability, and student enrollment.{% endtrans %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Algorithm Status Card -->
|
<!-- Algorithm Status & Commission Selection -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-lg-4">
|
<!-- Algorithm Status Card -->
|
||||||
<div class="card algorithm-status">
|
<div class="col-lg-4 mb-4">
|
||||||
<div class="card-header bg-info text-white">
|
<div class="card border-0 shadow-sm algorithm-status">
|
||||||
<h5 class="mb-0">
|
<div class="card-header bg-body-tertiary border-0 py-3">
|
||||||
<i class="fas fa-cogs me-2"></i>
|
<h5 class="mb-0 h6 fw-bold">
|
||||||
Algorithm Status
|
<i class="bi bi-gear-wide-connected text-primary me-2"></i>
|
||||||
|
{% trans %}Algorithm Status{% endtrans %}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<div class="mb-4">
|
||||||
<small class="text-muted">System Status</small>
|
<small class="text-muted text-uppercase fw-semibold d-block mb-2">{% trans %}System Status{% endtrans %}</small>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center py-1 border-bottom border-body-secondary">
|
||||||
<span>Available Classrooms</span>
|
<span>{% trans %}Available Classrooms{% endtrans %}</span>
|
||||||
<span class="badge bg-success" id="available-classrooms">--</span>
|
<span class="badge bg-success" id="available-classrooms">--</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center py-1 border-bottom border-body-secondary">
|
||||||
<span>Active Commissions</span>
|
<span>{% trans %}Active Commissions{% endtrans %}</span>
|
||||||
<span class="badge bg-info" id="active-commissions">--</span>
|
<span class="badge bg-info" id="active-commissions">--</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center py-1">
|
||||||
<span>Existing Reservations</span>
|
<span>{% trans %}Existing Reservations{% endtrans %}</span>
|
||||||
<span class="badge bg-secondary" id="existing-reservations">--</span>
|
<span class="badge bg-secondary" id="existing-reservations">--</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-4">
|
||||||
<small class="text-muted">Algorithm Configuration</small>
|
<small class="text-muted text-uppercase fw-semibold d-block mb-2">{% trans %}Algorithm Configuration{% endtrans %}</small>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center py-1 border-bottom border-body-secondary">
|
||||||
<span>Population Size</span>
|
<span>{% trans %}Population Size{% endtrans %}</span>
|
||||||
<span class="text-muted">50</span>
|
<span class="text-muted">50</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center py-1 border-bottom border-body-secondary">
|
||||||
<span>Generations</span>
|
<span>{% trans %}Generations{% endtrans %}</span>
|
||||||
<span class="text-muted">100</span>
|
<span class="text-muted">100</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center py-1">
|
||||||
<span>Mutation Rate</span>
|
<span>{% trans %}Mutation Rate{% endtrans %}</span>
|
||||||
<span class="text-muted">10%</span>
|
<span class="text-muted">10%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-primary w-100" onclick="geneticManager.getAlgorithmStatus()">
|
<button class="btn btn-sm btn-outline-primary w-100" onclick="geneticManager.getAlgorithmStatus()">
|
||||||
<i class="fas fa-sync-alt me-1"></i>
|
<i class="bi bi-arrow-clockwise me-1"></i>
|
||||||
Refresh Status
|
{% trans %}Refresh Status{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,34 +133,27 @@
|
|||||||
|
|
||||||
<!-- Commission Selection -->
|
<!-- Commission Selection -->
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<div class="card">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0 h6 fw-bold">
|
||||||
<i class="fas fa-list-check me-2"></i>
|
<i class="bi bi-list-check text-primary me-2"></i>
|
||||||
Select Commissions to Optimize
|
{% trans %}Select Commissions to Optimize{% endtrans %}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
<div class="d-flex gap-2">
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="d-flex gap-2 mb-2">
|
|
||||||
<button class="btn btn-sm btn-outline-primary" onclick="selectAllCommissions()">
|
<button class="btn btn-sm btn-outline-primary" onclick="selectAllCommissions()">
|
||||||
Select All
|
{% trans %}Select All{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="deselectAllCommissions()">
|
<button class="btn btn-sm btn-outline-secondary" onclick="deselectAllCommissions()">
|
||||||
Deselect All
|
{% trans %}Deselect All{% endtrans %}
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-outline-info" onclick="filterCommissions()">
|
|
||||||
<i class="fas fa-filter me-1"></i>
|
|
||||||
Filter
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
<div id="commissions-list" class="row">
|
<div id="commissions-list" class="row">
|
||||||
<!-- Commissions will be loaded here -->
|
<!-- Commissions will be loaded here -->
|
||||||
<div class="col-12 text-center text-muted">
|
<div class="col-12 text-center text-muted py-5">
|
||||||
<i class="fas fa-spinner fa-spin me-2"></i>
|
<div class="spinner-border spinner-border-sm me-2 text-primary" role="status"></div>
|
||||||
Loading commissions...
|
{% trans %}Loading commissions...{% endtrans %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,45 +164,45 @@
|
|||||||
<!-- Optimization Controls -->
|
<!-- Optimization Controls -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-header bg-primary text-white">
|
<div class="card-header bg-body-tertiary border-0 py-3">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0 h6 fw-bold">
|
||||||
<i class="fas fa-play-circle me-2"></i>
|
<i class="bi bi-sliders text-primary me-2"></i>
|
||||||
Optimization Controls
|
{% trans %}Optimization Controls{% endtrans %}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="start-date" class="form-label">Start Date Range</label>
|
<label for="start-date" class="form-label">{% trans %}Start Date Range{% endtrans %}</label>
|
||||||
<input type="datetime-local" class="form-control" id="start-date" name="start_date">
|
<input type="datetime-local" class="form-control" id="start-date" name="start_date">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="end-date" class="form-label">End Date Range</label>
|
<label for="end-date" class="form-label">{% trans %}End Date Range{% endtrans %}</label>
|
||||||
<input type="datetime-local" class="form-control" id="end-date" name="end_date">
|
<input type="datetime-local" class="form-control" id="end-date" name="end_date">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-2 flex-wrap">
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
<button class="btn btn-info" id="preview-optimization-btn">
|
<button class="btn btn-info text-white" id="preview-optimization-btn">
|
||||||
<i class="fas fa-eye me-2"></i>
|
<i class="bi bi-eye me-1"></i>
|
||||||
Preview Optimization
|
{% trans %}Preview Optimization{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-success" id="optimize-assignments-btn">
|
<button class="btn btn-success" id="optimize-assignments-btn">
|
||||||
<i class="fas fa-rocket me-2"></i>
|
<i class="bi bi-play-circle me-1"></i>
|
||||||
Run Full Optimization
|
{% trans %}Run Full Optimization{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-warning" id="apply-optimization-btn" style="display: none;">
|
<button class="btn btn-warning" id="apply-optimization-btn" style="display: none;">
|
||||||
<i class="fas fa-check me-2"></i>
|
<i class="bi bi-check-circle me-1"></i>
|
||||||
Apply Reservations
|
{% trans %}Apply Reservations{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" onclick="resetOptimization()">
|
<button class="btn btn-outline-secondary" onclick="resetOptimization()">
|
||||||
<i class="fas fa-undo me-2"></i>
|
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||||
Reset
|
{% trans %}Reset{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,13 +213,8 @@
|
|||||||
<!-- Results Section -->
|
<!-- Results Section -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<!-- Preview Results -->
|
|
||||||
<div id="preview-results"></div>
|
<div id="preview-results"></div>
|
||||||
|
|
||||||
<!-- Optimization Results -->
|
|
||||||
<div id="optimization-results"></div>
|
<div id="optimization-results"></div>
|
||||||
|
|
||||||
<!-- Alert Container -->
|
|
||||||
<div id="alert-container"></div>
|
<div id="alert-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -244,11 +223,9 @@
|
|||||||
<!-- Loading Spinner Overlay -->
|
<!-- Loading Spinner Overlay -->
|
||||||
<div class="spinner-overlay" id="loading-spinner">
|
<div class="spinner-overlay" id="loading-spinner">
|
||||||
<div class="text-center text-white">
|
<div class="text-center text-white">
|
||||||
<div class="spinner-border" style="width: 3rem; height: 3rem;" role="status">
|
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status"></div>
|
||||||
<span class="visually-hidden">Optimizing...</span>
|
<p class="mt-3 mb-1 fw-bold fs-5">{% trans %}Running genetic algorithm optimization...{% endtrans %}</p>
|
||||||
</div>
|
<small class="text-white-50">{% trans %}This may take a few moments{% endtrans %}</small>
|
||||||
<p class="mt-3 mb-0">Running genetic algorithm optimization...</p>
|
|
||||||
<small class="d-block mt-2">This may take a few moments</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -257,17 +234,17 @@
|
|||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="detailsModalLabel">Reservation Details</h5>
|
<h5 class="modal-title" id="detailsModalLabel">{% trans %}Reservation Details{% endtrans %}</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<!-- Content will be populated dynamically -->
|
<!-- Content populated dynamically -->
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans %}Close{% endtrans %}</button>
|
||||||
<button type="button" class="btn btn-primary" onclick="downloadReservations()">
|
<button type="button" class="btn btn-primary" onclick="downloadReservations()">
|
||||||
<i class="fas fa-download me-2"></i>
|
<i class="bi bi-download me-1"></i>
|
||||||
Download
|
{% trans %}Download{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,7 +255,6 @@
|
|||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="{{ url_for('static', filename='js/genetic-algorithm.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/genetic-algorithm.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
// Additional utility functions for the genetic optimizer interface
|
|
||||||
function selectAllCommissions() {
|
function selectAllCommissions() {
|
||||||
const checkboxes = document.querySelectorAll('input[name="commission_ids"]');
|
const checkboxes = document.querySelectorAll('input[name="commission_ids"]');
|
||||||
checkboxes.forEach(cb => {
|
checkboxes.forEach(cb => {
|
||||||
@@ -306,78 +282,45 @@ function updateCommissionCard(commissionId, selected) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterCommissions() {
|
|
||||||
// Implement filtering logic
|
|
||||||
console.log('Filter commissions feature not implemented yet');
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetOptimization() {
|
function resetOptimization() {
|
||||||
deselectAllCommissions();
|
deselectAllCommissions();
|
||||||
geneticManager.clearResults();
|
if (window.geneticManager) {
|
||||||
geneticManager.clearPreview();
|
geneticManager.clearResults && geneticManager.clearResults();
|
||||||
|
geneticManager.clearPreview && geneticManager.clearPreview();
|
||||||
|
}
|
||||||
document.getElementById('start-date').value = '';
|
document.getElementById('start-date').value = '';
|
||||||
document.getElementById('end-date').value = '';
|
document.getElementById('end-date').value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadReservations() {
|
|
||||||
const reservations = geneticManager.getPendingReservations();
|
|
||||||
if (!reservations || reservations.length === 0) {
|
|
||||||
alert('No reservations to download');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create CSV content
|
|
||||||
const headers = ['Commission ID', 'Classroom ID', 'Purpose', 'Start Time', 'End Time', 'Expected Attendees'];
|
|
||||||
const csvContent = [
|
|
||||||
headers.join(','),
|
|
||||||
...reservations.map(res => [
|
|
||||||
res.commission_id,
|
|
||||||
res.classroom_id,
|
|
||||||
`"${res.purpose}"`,
|
|
||||||
new Date(res.start_time).toISOString(),
|
|
||||||
new Date(res.end_time).toISOString(),
|
|
||||||
res.expected_attendees
|
|
||||||
].join(','))
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
// Download file
|
|
||||||
const blob = new Blob([csvContent], { type: 'text/csv' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `optimization_results_${new Date().toISOString().split('T')[0]}.csv`;
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load commissions on page load
|
|
||||||
document.addEventListener('DOMContentLoaded', async function() {
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
await loadCommissions();
|
await loadCommissions();
|
||||||
|
if (window.geneticManager) {
|
||||||
await geneticManager.getAlgorithmStatus();
|
await geneticManager.getAlgorithmStatus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadCommissions() {
|
async function loadCommissions() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/genetic/commissions');
|
const response = await fetch('/api/genetic/commissions');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const commissionsList = document.getElementById('commissions-list');
|
const commissionsList = document.getElementById('commissions-list');
|
||||||
|
const isSpanish = '{{ session.get("language", "es") }}' === 'es';
|
||||||
|
|
||||||
if (data.success && data.commissions && data.commissions.length > 0) {
|
if (data.success && data.commissions && data.commissions.length > 0) {
|
||||||
const commissionsHtml = data.commissions.map(commission => `
|
const commissionsHtml = data.commissions.map(commission => `
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<div class="card commission-item" data-commission-id="${commission.id}">
|
<div class="card commission-item border-0 shadow-sm" data-commission-id="${commission.id}">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" name="commission_ids"
|
<input class="form-check-input" type="checkbox" name="commission_ids"
|
||||||
value="${commission.id}" id="commission-${commission.id}"
|
value="${commission.id}" id="commission-${commission.id}"
|
||||||
onchange="updateCommissionCard('${commission.id}', this.checked)">
|
onchange="updateCommissionCard('${commission.id}', this.checked)">
|
||||||
<label class="form-check-label" for="commission-${commission.id}">
|
<label class="form-check-label" for="commission-${commission.id}">
|
||||||
<strong>${commission.get_full_code || commission.code}</strong>
|
<strong class="text-primary">${commission.get_full_code || commission.code}</strong>
|
||||||
<br>
|
<br>
|
||||||
<small class="text-muted">${commission.subject?.name || 'Unknown Subject'}</small>
|
<small class="text-muted">${commission.subject?.name || (isSpanish ? 'Materia Desconocida' : 'Unknown Subject')}</small>
|
||||||
<br>
|
<br>
|
||||||
<span class="badge bg-info">${commission.current_students || 0}/${commission.max_students} students</span>
|
<span class="badge bg-info mt-1">${commission.current_students || 0}/${commission.max_students} ${isSpanish ? 'estudiantes' : 'students'}</span>
|
||||||
${commission.teacher_name ? `<span class="badge bg-secondary ms-2">${commission.teacher_name}</span>` : ''}
|
${commission.teacher_name ? `<span class="badge bg-secondary ms-2">${commission.teacher_name}</span>` : ''}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -389,46 +332,21 @@ const response = await fetch('/api/genetic/commissions');
|
|||||||
commissionsList.innerHTML = commissionsHtml;
|
commissionsList.innerHTML = commissionsHtml;
|
||||||
} else {
|
} else {
|
||||||
commissionsList.innerHTML = `
|
commissionsList.innerHTML = `
|
||||||
<div class="col-12 text-center text-muted">
|
<div class="col-12 text-center text-muted py-5">
|
||||||
<i class="fas fa-info-circle me-2"></i>
|
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
|
||||||
No active commissions found.
|
${isSpanish ? 'No se encontraron comisiones activas.' : 'No active commissions found.'}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading commissions:', error);
|
console.error('Error loading commissions:', error);
|
||||||
document.getElementById('commissions-list').innerHTML = `
|
document.getElementById('commissions-list').innerHTML = `
|
||||||
<div class="col-12 text-center text-danger">
|
<div class="col-12 text-center text-danger py-4">
|
||||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
<i class="bi bi-exclamation-triangle fs-2 d-block mb-2"></i>
|
||||||
Error loading commissions: ${error.message}
|
Error al cargar comisiones: ${error.message}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override the genetic algorithm status handler
|
|
||||||
const originalGetAlgorithmStatus = geneticManager.getAlgorithmStatus;
|
|
||||||
geneticManager.getAlgorithmStatus = async function() {
|
|
||||||
try {
|
|
||||||
const result = await originalGetAlgorithmStatus.call(this);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
const data = result.data;
|
|
||||||
document.getElementById('available-classrooms').textContent = data.system_status.available_classrooms;
|
|
||||||
document.getElementById('active-commissions').textContent = data.system_status.active_commissions;
|
|
||||||
document.getElementById('existing-reservations').textContent = data.system_status.confirmed_reservations;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating algorithm status:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Override the loading function to use our custom spinner
|
|
||||||
geneticManager.showLoading = function(show) {
|
|
||||||
const spinner = document.getElementById('loading-spinner');
|
|
||||||
if (spinner) {
|
|
||||||
spinner.style.display = show ? 'flex' : 'none';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{% if config.LANGUAGES|length > 1 %}
|
{% if config.LANGUAGES|length > 1 %}
|
||||||
<div class="dropdown language-switcher">
|
<div class="dropdown language-switcher">
|
||||||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" id="languageDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" id="languageDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="bi bi-globe"></i>
|
<i class="bi bi-globe me-1"></i>
|
||||||
{% if session.language == 'es' or (session.language is none and request.accept_languages.best_match(['es', 'en']) == 'es') %}
|
{% if session.language == 'es' or (session.language is none and request.accept_languages.best_match(['es', 'en']) == 'es') %}
|
||||||
Spanish
|
Español
|
||||||
{% else %}
|
{% else %}
|
||||||
English
|
English
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Schedule Calendar - Edu-Space Admin{% endblock %}
|
{% block title %}{% trans %}Reservations Calendar{% endtrans %} - Edu-Space Admin{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="container-fluid pt-5 mt-4">
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h1 class="h3 mb-0">
|
<h1 class="h3 mb-0">
|
||||||
<i class="bi bi-calendar3 text-primary"></i> Schedule Calendar
|
<i class="bi bi-calendar3 text-primary"></i> {% trans %}Reservations Calendar{% endtrans %}
|
||||||
<small class="text-muted ms-2">View & Manage Reservations</small>
|
<small class="text-muted ms-2">{% trans %}View & Manage Reservations{% endtrans %}</small>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-header bg-white border-0">
|
<div class="card-header bg-body-tertiary border-0 py-3">
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0 h6 fw-bold">
|
||||||
<i class="bi bi-calendar-week text-info"></i> Reservations Calendar
|
<i class="bi bi-calendar-week text-info me-2"></i> {% trans %}Reservations Calendar{% endtrans %}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 text-end">
|
<div class="col-md-6 text-end">
|
||||||
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-primary">
|
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-circle"></i> Add Reservation
|
<i class="bi bi-plus-circle"></i> {% trans %}Add Reservation{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-secondary">
|
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-secondary">
|
||||||
<i class="bi bi-list"></i> List View
|
<i class="bi bi-list"></i> {% trans %}List View{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,11 +37,11 @@
|
|||||||
<div class="d-flex gap-3 flex-wrap">
|
<div class="d-flex gap-3 flex-wrap">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span class="badge bg-success me-2" style="width: 20px; height: 20px;"></span>
|
<span class="badge bg-success me-2" style="width: 20px; height: 20px;"></span>
|
||||||
<small class="text-muted">Confirmed</small>
|
<small class="text-muted">{% trans %}Confirmed{% endtrans %}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span class="badge bg-warning me-2" style="width: 20px; height: 20px;"></span>
|
<span class="badge bg-warning me-2" style="width: 20px; height: 20px;"></span>
|
||||||
<small class="text-muted">Pending</small>
|
<small class="text-muted">{% trans %}Pending{% endtrans %}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,18 +56,19 @@
|
|||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Reservation Details</h5>
|
<h5 class="modal-title">{% trans %}Reservation Details{% endtrans %}</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" id="reservationDetails">
|
<div class="modal-body" id="reservationDetails">
|
||||||
<!-- Content will be loaded dynamically -->
|
<!-- Content will be loaded dynamically -->
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans %}Close{% endtrans %}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
@@ -95,11 +97,14 @@
|
|||||||
|
|
||||||
{% block extra_js %}
|
{% 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/main.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.0/locales-all.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var calendarEl = document.getElementById('calendar');
|
var calendarEl = document.getElementById('calendar');
|
||||||
|
var currentLang = '{{ session.get("language", "es") }}';
|
||||||
|
|
||||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
|
locale: currentLang,
|
||||||
initialView: 'timeGridWeek',
|
initialView: 'timeGridWeek',
|
||||||
headerToolbar: {
|
headerToolbar: {
|
||||||
left: 'prev,next today',
|
left: 'prev,next today',
|
||||||
@@ -115,16 +120,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
slotMaxTime: '22:00:00',
|
slotMaxTime: '22:00:00',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
eventClick: function(info) {
|
eventClick: function(info) {
|
||||||
// Prevent default link behavior
|
|
||||||
if (info.event.url) {
|
if (info.event.url) {
|
||||||
// Load reservation details in modal instead of navigation
|
|
||||||
loadReservationDetails(info.event.url);
|
loadReservationDetails(info.event.url);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
events: '{{ url_for("schedule.calendar_data") }}',
|
events: '{{ url_for("schedule.calendar_data") }}',
|
||||||
loading: function(bool) {
|
loading: function(bool) {
|
||||||
// Show loading indicator if needed
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,38 +135,39 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
function loadReservationDetails(url) {
|
function loadReservationDetails(url) {
|
||||||
$.get(url)
|
$.get(url)
|
||||||
.done(function(data) {
|
.done(function(data) {
|
||||||
|
var isEs = currentLang === 'es';
|
||||||
var html = `
|
var html = `
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h6 class="text-muted mb-2">Classroom</h6>
|
<h6 class="text-muted mb-2">${isEs ? 'Aula' : 'Classroom'}</h6>
|
||||||
<p class="fw-bold">${data.classroom.code} - ${data.classroom.name}</p>
|
<p class="fw-bold">${data.classroom.code} - ${data.classroom.name || ''}</p>
|
||||||
<p><i class="bi bi-building text-primary"></i> ${data.classroom.building} - Floor ${data.classroom.floor}</p>
|
<p><i class="bi bi-building text-primary"></i> ${data.classroom.building} - ${isEs ? 'Piso' : 'Floor'} ${data.classroom.floor}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h6 class="text-muted mb-2">Subject</h6>
|
<h6 class="text-muted mb-2">${isEs ? 'Materia' : 'Subject'}</h6>
|
||||||
<p class="fw-bold">${data.commission.subject.code} - ${data.commission.subject.name}</p>
|
<p class="fw-bold">${data.commission.subject.code} - ${data.commission.subject.name}</p>
|
||||||
<p><i class="bi bi-person text-info"></i> ${data.commission.teacher_name}</p>
|
<p><i class="bi bi-person text-info"></i> ${data.commission.teacher_name}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h6 class="text-muted mb-2">Schedule</h6>
|
<h6 class="text-muted mb-2">${isEs ? 'Horario' : 'Schedule'}</h6>
|
||||||
<p><i class="bi bi-clock text-warning"></i> ${new Date(data.start_time).toLocaleString()}</p>
|
<p><i class="bi bi-clock text-warning"></i> ${new Date(data.start_time).toLocaleString()}</p>
|
||||||
<p><i class="bi bi-clock text-success"></i> ${new Date(data.end_time).toLocaleString()}</p>
|
<p><i class="bi bi-clock text-success"></i> ${new Date(data.end_time).toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h6 class="text-muted mb-2">Details</h6>
|
<h6 class="text-muted mb-2">${isEs ? 'Detalles' : 'Details'}</h6>
|
||||||
<p><i class="bi bi-people text-primary"></i> ${data.expected_attendees} attendees</p>
|
<p><i class="bi bi-people text-primary"></i> ${data.expected_attendees} ${isEs ? 'asistentes' : 'attendees'}</p>
|
||||||
<p><span class="badge bg-${data.status === 'CONFIRMED' ? 'success' : 'warning'}">${data.status}</span></p>
|
<p><span class="badge bg-${data.status === 'CONFIRMED' ? 'success' : 'warning'}">${data.status}</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${data.notes ? `<div class="row mt-3"><div class="col-12"><h6 class="text-muted mb-2">Notes</h6><p>${data.notes}</p></div></div>` : ''}
|
${data.notes ? `<div class="row mt-3"><div class="col-12"><h6 class="text-muted mb-2">${isEs ? 'Notas' : 'Notes'}</h6><p>${data.notes}</p></div></div>` : ''}
|
||||||
`;
|
`;
|
||||||
$('#reservationDetails').html(html);
|
$('#reservationDetails').html(html);
|
||||||
$('#reservationModal').modal('show');
|
$('#reservationModal').modal('show');
|
||||||
})
|
})
|
||||||
.fail(function() {
|
.fail(function() {
|
||||||
$('#reservationDetails').html('<p class="text-danger">Error loading reservation details.</p>');
|
$('#reservationDetails').html('<p class="text-danger">${isEs ? "Error al cargar los detalles de la reserva." : "Error loading reservation details."}</p>');
|
||||||
$('#reservationModal').modal('show');
|
$('#reservationModal').modal('show');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
{% if subject.description %}
|
{% if subject.description %}
|
||||||
<div class="card-footer bg-light">
|
<div class="card-footer bg-light">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<i class="bi bi-info-circle"></i> {{ subject.description|truncatewords(20) }}
|
<i class="bi bi-info-circle"></i> {{ subject.description|truncate(100) }}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -180,8 +180,8 @@
|
|||||||
<i class="bi bi-inbox fs-1 text-muted d-block mb-3"></i>
|
<i class="bi bi-inbox fs-1 text-muted d-block mb-3"></i>
|
||||||
<h4 class="text-muted">No Subjects Found</h4>
|
<h4 class="text-muted">No Subjects Found</h4>
|
||||||
<p class="text-muted">There are no active subjects in the system.</p>
|
<p class="text-muted">There are no active subjects in the system.</p>
|
||||||
<a href="{{ url_for('classrooms.index') }}" class="btn btn-primary">
|
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-circle"></i> Add New Subject
|
<i class="bi bi-door-open"></i> View Classrooms
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,20 +8,19 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h1 class="h3">
|
<h1 class="h3">
|
||||||
<i class="bi bi-calendar-today text-info"></i> Today's Schedule
|
<i class="bi bi-calendar-today text-info"></i> {% trans %}Today's Schedule{% endtrans %}
|
||||||
<small class="text-muted ms-2">{{ today.strftime('%B %d, %Y') }}</small>
|
<small class="text-muted ms-2">{{ today.strftime('%B %d, %Y') }}</small>
|
||||||
</h1>
|
</h1>
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-success">
|
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-success">
|
||||||
<i class="bi bi-plus-circle"></i> New Reservation
|
<i class="bi bi-plus-circle"></i> {% trans %}New Reservation{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-primary ms-2">
|
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-primary ms-2">
|
||||||
<i class="bi bi-list"></i> All Reservations
|
<i class="bi bi-list"></i> {% trans %}All Reservations{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
{# Access to Genetic Algorithm Optimizer - Only for admin/teacher roles #}
|
|
||||||
{% if current_user and current_user.role in ['admin', 'teacher'] %}
|
{% if current_user and current_user.role in ['admin', 'teacher'] %}
|
||||||
<a href="{{ url_for('main.genetic_optimizer') }}" class="btn btn-info text-white ms-2">
|
<a href="{{ url_for('genetic_algorithm_web.genetic_optimizer') }}" class="btn btn-info text-white ms-2">
|
||||||
<i class="bi bi-cpu"></i> AI Optimizer
|
<i class="bi bi-cpu"></i> {% trans %}AI Room Optimizer{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="text-muted mb-2">Total Reservations</h6>
|
<h6 class="text-muted mb-2">{% trans %}Total Reservations{% endtrans %}</h6>
|
||||||
<h2 class="mb-0">{{ stats.total_reservations }}</h2>
|
<h2 class="mb-0">{{ stats.total_reservations }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-info bg-opacity-10 rounded-circle p-3">
|
<div class="bg-info bg-opacity-10 rounded-circle p-3">
|
||||||
@@ -52,7 +51,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="text-muted mb-2">Confirmed</h6>
|
<h6 class="text-muted mb-2">{% trans %}Confirmed{% endtrans %}</h6>
|
||||||
<h2 class="mb-0">{{ stats.confirmed }}</h2>
|
<h2 class="mb-0">{{ stats.confirmed }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-success bg-opacity-10 rounded-circle p-3">
|
<div class="bg-success bg-opacity-10 rounded-circle p-3">
|
||||||
@@ -68,7 +67,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="text-muted mb-2">Pending</h6>
|
<h6 class="text-muted mb-2">{% trans %}Pending{% endtrans %}</h6>
|
||||||
<h2 class="mb-0">{{ stats.pending }}</h2>
|
<h2 class="mb-0">{{ stats.pending }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-warning bg-opacity-10 rounded-circle p-3">
|
<div class="bg-warning bg-opacity-10 rounded-circle p-3">
|
||||||
@@ -84,7 +83,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="text-muted mb-2">Active Classrooms</h6>
|
<h6 class="text-muted mb-2">{% trans %}Active Classrooms{% endtrans %}</h6>
|
||||||
<h2 class="mb-0">{{ stats.occupied_classrooms }}/{{ stats.total_classrooms }}</h2>
|
<h2 class="mb-0">{{ stats.occupied_classrooms }}/{{ stats.total_classrooms }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-primary bg-opacity-10 rounded-circle p-3">
|
<div class="bg-primary bg-opacity-10 rounded-circle p-3">
|
||||||
@@ -340,21 +339,20 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
<i class="bi bi-calendar-x text-muted" style="font-size: 4rem;"></i>
|
<i class="bi bi-calendar-x text-muted" style="font-size: 4rem;"></i>
|
||||||
<h4 class="mt-3 text-muted">No reservations scheduled for today</h4>
|
<h4 class="mt-3 text-muted">{% trans %}No reservations scheduled for today{% endtrans %}</h4>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
There are no classroom reservations for {{ today.strftime('%B %d, %Y') }}.
|
{% trans %}There are no classroom reservations for today.{% endtrans %}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-success">
|
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-success">
|
||||||
<i class="bi bi-plus-circle"></i> Schedule First Class
|
<i class="bi bi-plus-circle"></i> {% trans %}Schedule First Class{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-primary ms-2">
|
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-primary ms-2">
|
||||||
<i class="bi bi-calendar-week"></i>View Week Schedule
|
<i class="bi bi-calendar-week"></i> {% trans %}View Week Schedule{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
{# Access to Genetic Algorithm Optimizer - Only for admin/teacher roles #}
|
|
||||||
{% if current_user and current_user.role in ['admin', 'teacher'] %}
|
{% if current_user and current_user.role in ['admin', 'teacher'] %}
|
||||||
<a href="{{ url_for('main.genetic_optimizer') }}" class="btn btn-info text-white ms-2">
|
<a href="{{ url_for('genetic_algorithm_web.genetic_optimizer') }}" class="btn btn-info text-white ms-2">
|
||||||
<i class="bi bi-cpu"></i> AI Room Optimizer
|
<i class="bi bi-cpu"></i> {% trans %}AI Room Optimizer{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
[python: **.py]
|
[python: app/**.py]
|
||||||
[jinja2: **/templates/**.html]
|
[jinja2.ext.babel_extract: app/templates/**.html]
|
||||||
extensions=jinja2.ext.i18n
|
extensions=jinja2.ext.i18n
|
||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
|
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'postgresql://postgres:password@localhost:5432/classrooms_db'
|
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'postgresql://postgres:password@localhost:5432/classrooms_db'
|
||||||
@@ -18,4 +20,4 @@ class Config:
|
|||||||
LANGUAGES = ['en', 'es']
|
LANGUAGES = ['en', 'es']
|
||||||
BABEL_DEFAULT_LOCALE = 'en'
|
BABEL_DEFAULT_LOCALE = 'en'
|
||||||
BABEL_DEFAULT_TIMEZONE = 'UTC'
|
BABEL_DEFAULT_TIMEZONE = 'UTC'
|
||||||
BABEL_TRANSLATION_DIRECTORIES = 'translations'
|
BABEL_TRANSLATION_DIRECTORIES = os.path.join(basedir, 'translations')
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
import sys
|
||||||
|
from app import create_app, db
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
def init_database():
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
print("⏳ Conectando a la base de datos y creando tablas...")
|
||||||
|
db.create_all()
|
||||||
|
print("✅ Tablas creadas en la base de datos con éxito.")
|
||||||
|
|
||||||
|
# Verificar o crear usuario administrador
|
||||||
|
admin = User.query.filter_by(email="admin@edu-space.com").first()
|
||||||
|
if not admin:
|
||||||
|
print("⏳ Creando usuario administrador inicial...")
|
||||||
|
admin = User(
|
||||||
|
email="admin@edu-space.com",
|
||||||
|
name="System Administrator",
|
||||||
|
role="ADMIN",
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
admin.set_password("admin123")
|
||||||
|
db.session.add(admin)
|
||||||
|
db.session.commit()
|
||||||
|
print("✅ Usuario inicial creado exitosamente:")
|
||||||
|
print(" Email: admin@edu-space.com")
|
||||||
|
print(" Password: admin123")
|
||||||
|
else:
|
||||||
|
print("ℹ️ El usuario admin (admin@edu-space.com) ya existe en la base de datos.")
|
||||||
|
|
||||||
|
print("\n🚀 Base de datos inicializada y lista para usar.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Error al inicializar la base de datos: {str(e)}")
|
||||||
|
print("\nVerifica que:")
|
||||||
|
print("1. El servicio de PostgreSQL o MySQL esté corriendo.")
|
||||||
|
print("2. La base de datos 'classrooms_db' haya sido creada.")
|
||||||
|
print("3. La variable DATABASE_URL en el archivo .env tenga el usuario y contraseña correctos.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_database()
|
||||||
@@ -3,27 +3,219 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Edu-Space Admin 1.0\n"
|
"Project-Id-Version: Edu-Space Admin 1.0\n"
|
||||||
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2026-09-02 23:40-0300\n"
|
||||||
"PO-Revision-Date: 2024-01-01 00:00+0000\n"
|
"PO-Revision-Date: 2024-01-01 00:00+0000\n"
|
||||||
"Last-Translator: Admin <admin@edu-space.com>\n"
|
"Last-Translator: Admin <admin@edu-space.com>\n"
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
|
"Language-Team: en <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: pybabel\n"
|
"Generated-By: Babel 2.14.0\n"
|
||||||
|
|
||||||
|
#: app/forms/auth.py:7 app/templates/auth/profile.html:44
|
||||||
|
msgid "Email"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/auth.py:12
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Change Password"
|
||||||
|
|
||||||
|
#: app/forms/auth.py:13
|
||||||
|
msgid "Remember me"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/auth.py:14
|
||||||
|
msgid "Sign In"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:8 app/templates/classrooms/list.html:83
|
||||||
|
msgid "Building"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:12
|
||||||
|
msgid "Room Number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:16 app/templates/classrooms/add.html:148
|
||||||
|
msgid "Floor"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:20 app/templates/classrooms/list.html:79
|
||||||
|
msgid "Capacity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:24
|
||||||
|
msgid "Description"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:28 app/templates/auth/profile.html:31
|
||||||
|
#: app/templates/classrooms/list.html:73
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Active"
|
||||||
|
msgstr "Actions"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:29
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Save Classroom"
|
||||||
|
msgstr "Add Classroom"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:32
|
||||||
|
msgid "Resource Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:33
|
||||||
|
msgid "Projector"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:34
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Whiteboard"
|
||||||
|
msgstr "Dashboard"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:35
|
||||||
|
msgid "Computer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:36
|
||||||
|
msgid "Sound System"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:37
|
||||||
|
msgid "Microphone"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:38
|
||||||
|
msgid "Air Conditioner"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:39
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Other"
|
||||||
|
msgstr "Teacher"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:41
|
||||||
|
msgid "Brand"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:42
|
||||||
|
msgid "Model"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:43
|
||||||
|
msgid "Specifications"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:44
|
||||||
|
msgid "Functional"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:45
|
||||||
|
msgid "Save Resource"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:8 app/templates/auth/profile.html:130
|
||||||
|
#: app/templates/dashboard.html:155
|
||||||
|
msgid "Classroom"
|
||||||
|
msgstr "Classroom"
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:9
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Commission/Subject"
|
||||||
|
msgstr "Manage Commissions"
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:10
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Start Time"
|
||||||
|
msgstr "Date & Time"
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:11
|
||||||
|
#, fuzzy
|
||||||
|
msgid "End Time"
|
||||||
|
msgstr "Date & Time"
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:12 app/templates/auth/profile.html:131
|
||||||
|
msgid "Purpose"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:16
|
||||||
|
msgid "Expected Attendees"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:20
|
||||||
|
msgid "Notes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:24
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Make Reservation"
|
||||||
|
msgstr "New Reservation"
|
||||||
|
|
||||||
#: app/templates/base.html:6
|
#: app/templates/base.html:6
|
||||||
msgid "Edu-Space Admin"
|
msgid "Edu-Space Admin"
|
||||||
msgstr "Edu-Space Admin"
|
msgstr "Edu-Space Admin"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:3
|
#: app/templates/auth/profile.html:15 app/templates/base.html:34
|
||||||
msgid "Dashboard - Edu-Space Admin"
|
|
||||||
msgstr "Dashboard - Edu-Space Admin"
|
|
||||||
|
|
||||||
#: app/templates/dashboard.html:12
|
#: app/templates/dashboard.html:12
|
||||||
msgid "Dashboard"
|
msgid "Dashboard"
|
||||||
msgstr "Dashboard"
|
msgstr "Dashboard"
|
||||||
|
|
||||||
|
#: app/templates/base.html:39 app/templates/classrooms/list.html:3
|
||||||
|
#: app/templates/classrooms/list.html:11
|
||||||
|
msgid "Classrooms"
|
||||||
|
msgstr "Classrooms"
|
||||||
|
|
||||||
|
#: app/templates/base.html:43
|
||||||
|
msgid "All Classrooms"
|
||||||
|
msgstr "All Classrooms"
|
||||||
|
|
||||||
|
#: app/templates/base.html:46 app/templates/classrooms/add.html:3
|
||||||
|
#: app/templates/classrooms/add.html:11 app/templates/classrooms/list.html:15
|
||||||
|
#: app/templates/dashboard.html:111
|
||||||
|
msgid "Add Classroom"
|
||||||
|
msgstr "Add Classroom"
|
||||||
|
|
||||||
|
#: app/templates/base.html:52
|
||||||
|
msgid "Schedule"
|
||||||
|
msgstr "Schedule"
|
||||||
|
|
||||||
|
#: app/templates/base.html:56
|
||||||
|
msgid "Calendar View"
|
||||||
|
msgstr "Calendar View"
|
||||||
|
|
||||||
|
#: app/templates/base.html:59 app/templates/schedule/today.html:19
|
||||||
|
msgid "All Reservations"
|
||||||
|
msgstr "All Reservations"
|
||||||
|
|
||||||
|
#: app/templates/base.html:62 app/templates/dashboard.html:117
|
||||||
|
#: app/templates/schedule/today.html:16
|
||||||
|
msgid "New Reservation"
|
||||||
|
msgstr "New Reservation"
|
||||||
|
|
||||||
|
#: app/templates/base.html:65 app/templates/dashboard.html:120
|
||||||
|
#: app/templates/schedule/today.html:11
|
||||||
|
msgid "Today's Schedule"
|
||||||
|
msgstr "Today's Schedule"
|
||||||
|
|
||||||
|
#: app/templates/base.html:84
|
||||||
|
msgid "Profile"
|
||||||
|
msgstr "Profile"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:112 app/templates/base.html:87
|
||||||
|
msgid "Change Password"
|
||||||
|
msgstr "Change Password"
|
||||||
|
|
||||||
|
#: app/templates/base.html:91
|
||||||
|
msgid "Logout"
|
||||||
|
msgstr "Logout"
|
||||||
|
|
||||||
|
#: app/templates/dashboard.html:3
|
||||||
|
msgid "Dashboard - Edu-Space Admin"
|
||||||
|
msgstr "Dashboard - Edu-Space Admin"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:13
|
#: app/templates/dashboard.html:13
|
||||||
msgid "Overview"
|
msgid "Overview"
|
||||||
msgstr "Overview"
|
msgstr "Overview"
|
||||||
@@ -52,63 +244,422 @@ msgstr "Monthly Reservations"
|
|||||||
msgid "Quick Actions"
|
msgid "Quick Actions"
|
||||||
msgstr "Quick Actions"
|
msgstr "Quick Actions"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:111
|
|
||||||
msgid "Add Classroom"
|
|
||||||
msgstr "Add Classroom"
|
|
||||||
|
|
||||||
#: app/templates/dashboard.html:114
|
#: app/templates/dashboard.html:114
|
||||||
msgid "New Reservation"
|
msgid "Manage Commissions"
|
||||||
msgstr "New Reservation"
|
msgstr "Manage Commissions"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:117
|
#: app/templates/dashboard.html:123 app/templates/schedule/today.html:23
|
||||||
msgid "Today's Schedule"
|
#: app/templates/schedule/today.html:355
|
||||||
msgstr "Today's Schedule"
|
msgid "AI Room Optimizer"
|
||||||
|
msgstr "AI Room Optimizer"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:120
|
#: app/templates/dashboard.html:126
|
||||||
msgid "View Calendar"
|
msgid "View Calendar"
|
||||||
msgstr "View Calendar"
|
msgstr "View Calendar"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:135
|
#: app/templates/dashboard.html:141
|
||||||
msgid "Upcoming Reservations (Next 7 Days)"
|
msgid "Upcoming Reservations (Next 7 Days)"
|
||||||
msgstr "Upcoming Reservations (Next 7 Days)"
|
msgstr "Upcoming Reservations (Next 7 Days)"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:138
|
#: app/templates/dashboard.html:144
|
||||||
msgid "View All"
|
msgid "View All"
|
||||||
msgstr "View All"
|
msgstr "View All"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:148
|
#: app/templates/auth/profile.html:132 app/templates/dashboard.html:154
|
||||||
#: app/templates/dashboard.html:149
|
|
||||||
#: app/templates/dashboard.html:150
|
|
||||||
#: app/templates/dashboard.html:151
|
|
||||||
#: app/templates/dashboard.html:152
|
|
||||||
#: app/templates/dashboard.html:153
|
|
||||||
msgid "Date & Time"
|
msgid "Date & Time"
|
||||||
msgstr "Date & Time"
|
msgstr "Date & Time"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:149
|
#: app/templates/dashboard.html:156
|
||||||
msgid "Classroom"
|
|
||||||
msgstr "Classroom"
|
|
||||||
|
|
||||||
#: app/templates/dashboard.html:150
|
|
||||||
msgid "Subject"
|
msgid "Subject"
|
||||||
msgstr "Subject"
|
msgstr "Subject"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:151
|
#: app/templates/dashboard.html:157
|
||||||
msgid "Teacher"
|
msgid "Teacher"
|
||||||
msgstr "Teacher"
|
msgstr "Teacher"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:152
|
#: app/templates/auth/profile.html:133 app/templates/dashboard.html:158
|
||||||
msgid "Status"
|
msgid "Status"
|
||||||
msgstr "Status"
|
msgstr "Status"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:153
|
#: app/templates/dashboard.html:159
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr "Actions"
|
msgstr "Actions"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:171
|
#: app/templates/classrooms/list.html:112 app/templates/dashboard.html:177
|
||||||
msgid "View"
|
msgid "View"
|
||||||
msgstr "View"
|
msgstr "View"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:182
|
#: app/templates/dashboard.html:188
|
||||||
msgid "No upcoming reservations in the next 7 days"
|
msgid "No upcoming reservations in the next 7 days"
|
||||||
msgstr "No upcoming reservations in the next 7 days"
|
msgstr "No upcoming reservations in the next 7 days"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:3
|
||||||
|
#: app/templates/genetic_optimizer.html:70
|
||||||
|
#, fuzzy
|
||||||
|
msgid "AI Room Reservation Optimizer"
|
||||||
|
msgstr "AI Room Optimizer"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:73
|
||||||
|
msgid ""
|
||||||
|
"Use genetic algorithms to automatically optimize room assignments based "
|
||||||
|
"on capacity, availability, and student enrollment."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:90
|
||||||
|
msgid "Algorithm Status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:95
|
||||||
|
#, fuzzy
|
||||||
|
msgid "System Status"
|
||||||
|
msgstr "Status"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:97
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Available Classrooms"
|
||||||
|
msgstr "All Classrooms"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:101
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Active Commissions"
|
||||||
|
msgstr "Manage Commissions"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:105
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Existing Reservations"
|
||||||
|
msgstr "New Reservation"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:111
|
||||||
|
msgid "Algorithm Configuration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:113
|
||||||
|
msgid "Population Size"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:117
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Generations"
|
||||||
|
msgstr "All Reservations"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:121
|
||||||
|
msgid "Mutation Rate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:128
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Refresh Status"
|
||||||
|
msgstr "Status"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:140
|
||||||
|
msgid "Select Commissions to Optimize"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:144
|
||||||
|
msgid "Select All"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:147
|
||||||
|
msgid "Deselect All"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:156
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Loading commissions..."
|
||||||
|
msgstr "Manage Commissions"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:171
|
||||||
|
msgid "Optimization Controls"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:178
|
||||||
|
msgid "Start Date Range"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:184
|
||||||
|
msgid "End Date Range"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:193
|
||||||
|
msgid "Preview Optimization"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:197
|
||||||
|
msgid "Run Full Optimization"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:201
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Apply Reservations"
|
||||||
|
msgstr "All Reservations"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:205
|
||||||
|
msgid "Reset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:227
|
||||||
|
msgid "Running genetic algorithm optimization..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:228
|
||||||
|
msgid "This may take a few moments"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:237
|
||||||
|
#: app/templates/schedule/calendar.html:59
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Reservation Details"
|
||||||
|
msgstr "All Reservations"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:244
|
||||||
|
#: app/templates/schedule/calendar.html:66
|
||||||
|
msgid "Close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:247
|
||||||
|
msgid "Download"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/login.html:6
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Login - Edu-Space Admin"
|
||||||
|
msgstr "Edu-Space Admin"
|
||||||
|
|
||||||
|
#: app/templates/auth/login.html:51
|
||||||
|
msgid "Classroom Management System"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/login.html:73
|
||||||
|
msgid "Enter your email"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/login.html:84
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Enter your password"
|
||||||
|
msgstr "Change Password"
|
||||||
|
|
||||||
|
#: app/templates/auth/login.html:106
|
||||||
|
msgid "Secure Login • All access is logged"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:3
|
||||||
|
#, fuzzy
|
||||||
|
msgid "User Profile"
|
||||||
|
msgstr "Profile"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:12
|
||||||
|
#, fuzzy
|
||||||
|
msgid "My Profile"
|
||||||
|
msgstr "Profile"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:33 app/templates/classrooms/list.html:73
|
||||||
|
msgid "Inactive"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:48
|
||||||
|
msgid "Full Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:52
|
||||||
|
msgid "Access Role"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:56
|
||||||
|
msgid "Last Login"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:61
|
||||||
|
msgid "First login"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:66
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Registration Date"
|
||||||
|
msgstr "All Reservations"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:82
|
||||||
|
msgid "Language Preference"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:85
|
||||||
|
msgid ""
|
||||||
|
"If set, this language overrides the browser configuration for all your "
|
||||||
|
"sessions."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:91
|
||||||
|
msgid "Auto-detect from browser (Default)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:103
|
||||||
|
msgid "Save Preference"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:110
|
||||||
|
msgid "Account Security"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:122
|
||||||
|
#, fuzzy
|
||||||
|
msgid "My Recent Reservations"
|
||||||
|
msgstr "Monthly Reservations"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:14
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Back to Classrooms"
|
||||||
|
msgstr "Total Classrooms"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:25
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Classroom Information"
|
||||||
|
msgstr "Classroom"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:86
|
||||||
|
msgid "Optional: Add any additional details about this classroom..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:97
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Preview"
|
||||||
|
msgstr "View"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:100
|
||||||
|
msgid "Fill in the form to see a preview of the classroom code:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:103
|
||||||
|
msgid "Full location:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:111
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:129
|
||||||
|
msgid "Resources (Optional)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:133
|
||||||
|
msgid "You can add classroom resources after creating the classroom."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:30
|
||||||
|
msgid "Search by building, room, description..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:38
|
||||||
|
msgid "Show active classrooms only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:44
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Search"
|
||||||
|
msgstr "Teacher"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:47
|
||||||
|
msgid "Clear"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:99
|
||||||
|
msgid "resources"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:101
|
||||||
|
msgid "No resources registered"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:116
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:124
|
||||||
|
msgid "Deactivate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:126
|
||||||
|
msgid "Activate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:144
|
||||||
|
msgid "Previous"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:172
|
||||||
|
msgid "Next"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:185
|
||||||
|
#, fuzzy
|
||||||
|
msgid "No classrooms found"
|
||||||
|
msgstr "Classrooms"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:188
|
||||||
|
msgid "No classrooms match your search criteria."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:190
|
||||||
|
msgid "No classrooms have been registered yet."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:194
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Add First Classroom"
|
||||||
|
msgstr "Add Classroom"
|
||||||
|
|
||||||
|
#: app/templates/schedule/calendar.html:3
|
||||||
|
#: app/templates/schedule/calendar.html:10
|
||||||
|
#: app/templates/schedule/calendar.html:21
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Reservations Calendar"
|
||||||
|
msgstr "All Reservations"
|
||||||
|
|
||||||
|
#: app/templates/schedule/calendar.html:11
|
||||||
|
#, fuzzy
|
||||||
|
msgid "View & Manage Reservations"
|
||||||
|
msgstr "New Reservation"
|
||||||
|
|
||||||
|
#: app/templates/schedule/calendar.html:26
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Add Reservation"
|
||||||
|
msgstr "All Reservations"
|
||||||
|
|
||||||
|
#: app/templates/schedule/calendar.html:29
|
||||||
|
#, fuzzy
|
||||||
|
msgid "List View"
|
||||||
|
msgstr "View"
|
||||||
|
|
||||||
|
#: app/templates/schedule/calendar.html:40 app/templates/schedule/today.html:54
|
||||||
|
msgid "Confirmed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/schedule/calendar.html:44 app/templates/schedule/today.html:70
|
||||||
|
msgid "Pending"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/schedule/today.html:38
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Total Reservations"
|
||||||
|
msgstr "All Reservations"
|
||||||
|
|
||||||
|
#: app/templates/schedule/today.html:86
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Active Classrooms"
|
||||||
|
msgstr "All Classrooms"
|
||||||
|
|
||||||
|
#: app/templates/schedule/today.html:342
|
||||||
|
msgid "No reservations scheduled for today"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/schedule/today.html:344
|
||||||
|
msgid "There are no classroom reservations for today."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/schedule/today.html:348
|
||||||
|
msgid "Schedule First Class"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/schedule/today.html:351
|
||||||
|
#, fuzzy
|
||||||
|
msgid "View Week Schedule"
|
||||||
|
msgstr "Schedule"
|
||||||
|
|
||||||
|
|||||||
@@ -3,27 +3,210 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Edu-Space Admin 1.0\n"
|
"Project-Id-Version: Edu-Space Admin 1.0\n"
|
||||||
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2026-09-02 23:40-0300\n"
|
||||||
"PO-Revision-Date: 2024-01-01 00:00+0000\n"
|
"PO-Revision-Date: 2024-01-01 00:00+0000\n"
|
||||||
"Last-Translator: Admin <admin@edu-space.com>\n"
|
"Last-Translator: Admin <admin@edu-space.com>\n"
|
||||||
"Language: es\n"
|
"Language: es\n"
|
||||||
|
"Language-Team: es <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: pybabel\n"
|
"Generated-By: Babel 2.14.0\n"
|
||||||
|
|
||||||
|
#: app/forms/auth.py:7 app/templates/auth/profile.html:44
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "Correo Electrónico"
|
||||||
|
|
||||||
|
#: app/forms/auth.py:12
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Contraseña"
|
||||||
|
|
||||||
|
#: app/forms/auth.py:13
|
||||||
|
msgid "Remember me"
|
||||||
|
msgstr "Recordarme"
|
||||||
|
|
||||||
|
#: app/forms/auth.py:14
|
||||||
|
msgid "Sign In"
|
||||||
|
msgstr "Iniciar Sesión"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:8 app/templates/classrooms/list.html:83
|
||||||
|
msgid "Building"
|
||||||
|
msgstr "Edificio"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:12
|
||||||
|
msgid "Room Number"
|
||||||
|
msgstr "Número de Aula"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:16 app/templates/classrooms/add.html:148
|
||||||
|
msgid "Floor"
|
||||||
|
msgstr "Piso"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:20 app/templates/classrooms/list.html:79
|
||||||
|
msgid "Capacity"
|
||||||
|
msgstr "Capacidad"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:24
|
||||||
|
msgid "Description"
|
||||||
|
msgstr "Descripción"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:28 app/templates/auth/profile.html:31
|
||||||
|
#: app/templates/classrooms/list.html:73
|
||||||
|
msgid "Active"
|
||||||
|
msgstr "Activa"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:29
|
||||||
|
msgid "Save Classroom"
|
||||||
|
msgstr "Guardar Aula"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:32
|
||||||
|
msgid "Resource Type"
|
||||||
|
msgstr "Tipo de Recurso"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:33
|
||||||
|
msgid "Projector"
|
||||||
|
msgstr "Proyector"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:34
|
||||||
|
msgid "Whiteboard"
|
||||||
|
msgstr "Pizarra"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:35
|
||||||
|
msgid "Computer"
|
||||||
|
msgstr "Computadora"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:36
|
||||||
|
msgid "Sound System"
|
||||||
|
msgstr "Sistema de Sonido"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:37
|
||||||
|
msgid "Microphone"
|
||||||
|
msgstr "Micrófono"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:38
|
||||||
|
msgid "Air Conditioner"
|
||||||
|
msgstr "Aire Acondicionado"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:39
|
||||||
|
msgid "Other"
|
||||||
|
msgstr "Otro"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:41
|
||||||
|
msgid "Brand"
|
||||||
|
msgstr "Marca"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:42
|
||||||
|
msgid "Model"
|
||||||
|
msgstr "Modelo"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:43
|
||||||
|
msgid "Specifications"
|
||||||
|
msgstr "Especificaciones"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:44
|
||||||
|
msgid "Functional"
|
||||||
|
msgstr "Funcional"
|
||||||
|
|
||||||
|
#: app/forms/classroom.py:45
|
||||||
|
msgid "Save Resource"
|
||||||
|
msgstr "Guardar Recurso"
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:8 app/templates/auth/profile.html:130
|
||||||
|
#: app/templates/dashboard.html:155
|
||||||
|
msgid "Classroom"
|
||||||
|
msgstr "Aula"
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:9
|
||||||
|
msgid "Commission/Subject"
|
||||||
|
msgstr "Comisión / Materia"
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:10
|
||||||
|
msgid "Start Time"
|
||||||
|
msgstr "Hora de Inicio"
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:11
|
||||||
|
msgid "End Time"
|
||||||
|
msgstr "Hora de Fin"
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:12 app/templates/auth/profile.html:131
|
||||||
|
msgid "Purpose"
|
||||||
|
msgstr "Propósito"
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:16
|
||||||
|
msgid "Expected Attendees"
|
||||||
|
msgstr "Asistentes Estimados"
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:20
|
||||||
|
msgid "Notes"
|
||||||
|
msgstr "Notas"
|
||||||
|
|
||||||
|
#: app/forms/reservation.py:24
|
||||||
|
msgid "Make Reservation"
|
||||||
|
msgstr "Realizar Reserva"
|
||||||
|
|
||||||
#: app/templates/base.html:6
|
#: app/templates/base.html:6
|
||||||
msgid "Edu-Space Admin"
|
msgid "Edu-Space Admin"
|
||||||
msgstr "Admin Edu-Space"
|
msgstr "Admin Edu-Space"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:3
|
#: app/templates/auth/profile.html:15 app/templates/base.html:34
|
||||||
msgid "Dashboard - Edu-Space Admin"
|
|
||||||
msgstr "Panel - Admin Edu-Space"
|
|
||||||
|
|
||||||
#: app/templates/dashboard.html:12
|
#: app/templates/dashboard.html:12
|
||||||
msgid "Dashboard"
|
msgid "Dashboard"
|
||||||
msgstr "Panel"
|
msgstr "Panel"
|
||||||
|
|
||||||
|
#: app/templates/base.html:39 app/templates/classrooms/list.html:3
|
||||||
|
#: app/templates/classrooms/list.html:11
|
||||||
|
msgid "Classrooms"
|
||||||
|
msgstr "Aulas"
|
||||||
|
|
||||||
|
#: app/templates/base.html:43
|
||||||
|
msgid "All Classrooms"
|
||||||
|
msgstr "Todas las Aulas"
|
||||||
|
|
||||||
|
#: app/templates/base.html:46 app/templates/classrooms/add.html:3
|
||||||
|
#: app/templates/classrooms/add.html:11 app/templates/classrooms/list.html:15
|
||||||
|
#: app/templates/dashboard.html:111
|
||||||
|
msgid "Add Classroom"
|
||||||
|
msgstr "Agregar Aula"
|
||||||
|
|
||||||
|
#: app/templates/base.html:52
|
||||||
|
msgid "Schedule"
|
||||||
|
msgstr "Horarios"
|
||||||
|
|
||||||
|
#: app/templates/base.html:56
|
||||||
|
msgid "Calendar View"
|
||||||
|
msgstr "Ver Calendario"
|
||||||
|
|
||||||
|
#: app/templates/base.html:59 app/templates/schedule/today.html:19
|
||||||
|
msgid "All Reservations"
|
||||||
|
msgstr "Todas las Reservas"
|
||||||
|
|
||||||
|
#: app/templates/base.html:62 app/templates/dashboard.html:117
|
||||||
|
#: app/templates/schedule/today.html:16
|
||||||
|
msgid "New Reservation"
|
||||||
|
msgstr "Nueva Reserva"
|
||||||
|
|
||||||
|
#: app/templates/base.html:65 app/templates/dashboard.html:120
|
||||||
|
#: app/templates/schedule/today.html:11
|
||||||
|
msgid "Today's Schedule"
|
||||||
|
msgstr "Horario de Hoy"
|
||||||
|
|
||||||
|
#: app/templates/base.html:84
|
||||||
|
msgid "Profile"
|
||||||
|
msgstr "Perfil"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:112 app/templates/base.html:87
|
||||||
|
msgid "Change Password"
|
||||||
|
msgstr "Cambiar Contraseña"
|
||||||
|
|
||||||
|
#: app/templates/base.html:91
|
||||||
|
msgid "Logout"
|
||||||
|
msgstr "Cerrar Sesión"
|
||||||
|
|
||||||
|
#: app/templates/dashboard.html:3
|
||||||
|
msgid "Dashboard - Edu-Space Admin"
|
||||||
|
msgstr "Panel - Admin Edu-Space"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:13
|
#: app/templates/dashboard.html:13
|
||||||
msgid "Overview"
|
msgid "Overview"
|
||||||
msgstr "Resumen"
|
msgstr "Resumen"
|
||||||
@@ -52,63 +235,397 @@ msgstr "Reservas Mensuales"
|
|||||||
msgid "Quick Actions"
|
msgid "Quick Actions"
|
||||||
msgstr "Acciones Rápidas"
|
msgstr "Acciones Rápidas"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:111
|
|
||||||
msgid "Add Classroom"
|
|
||||||
msgstr "Agregar Aula"
|
|
||||||
|
|
||||||
#: app/templates/dashboard.html:114
|
#: app/templates/dashboard.html:114
|
||||||
msgid "New Reservation"
|
msgid "Manage Commissions"
|
||||||
msgstr "Nueva Reserva"
|
msgstr "Gestionar Comisiones"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:117
|
#: app/templates/dashboard.html:123 app/templates/schedule/today.html:23
|
||||||
msgid "Today's Schedule"
|
#: app/templates/schedule/today.html:355
|
||||||
msgstr "Horario de Hoy"
|
msgid "AI Room Optimizer"
|
||||||
|
msgstr "Optimizador de Aulas IA"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:120
|
#: app/templates/dashboard.html:126
|
||||||
msgid "View Calendar"
|
msgid "View Calendar"
|
||||||
msgstr "Ver Calendario"
|
msgstr "Ver Calendario"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:135
|
#: app/templates/dashboard.html:141
|
||||||
msgid "Upcoming Reservations (Next 7 Days)"
|
msgid "Upcoming Reservations (Next 7 Days)"
|
||||||
msgstr "Próximas Reservas (Próximos 7 Días)"
|
msgstr "Próximas Reservas (Próximos 7 Días)"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:138
|
#: app/templates/dashboard.html:144
|
||||||
msgid "View All"
|
msgid "View All"
|
||||||
msgstr "Ver Todas"
|
msgstr "Ver Todas"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:148
|
#: app/templates/auth/profile.html:132 app/templates/dashboard.html:154
|
||||||
#: app/templates/dashboard.html:149
|
|
||||||
#: app/templates/dashboard.html:150
|
|
||||||
#: app/templates/dashboard.html:151
|
|
||||||
#: app/templates/dashboard.html:152
|
|
||||||
#: app/templates/dashboard.html:153
|
|
||||||
msgid "Date & Time"
|
msgid "Date & Time"
|
||||||
msgstr "Fecha y Hora"
|
msgstr "Fecha y Hora"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:149
|
#: app/templates/dashboard.html:156
|
||||||
msgid "Classroom"
|
|
||||||
msgstr "Aula"
|
|
||||||
|
|
||||||
#: app/templates/dashboard.html:150
|
|
||||||
msgid "Subject"
|
msgid "Subject"
|
||||||
msgstr "Materia"
|
msgstr "Materia"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:151
|
#: app/templates/dashboard.html:157
|
||||||
msgid "Teacher"
|
msgid "Teacher"
|
||||||
msgstr "Profesor"
|
msgstr "Profesor"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:152
|
#: app/templates/auth/profile.html:133 app/templates/dashboard.html:158
|
||||||
msgid "Status"
|
msgid "Status"
|
||||||
msgstr "Estado"
|
msgstr "Estado"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:153
|
#: app/templates/dashboard.html:159
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr "Acciones"
|
msgstr "Acciones"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:171
|
#: app/templates/classrooms/list.html:112 app/templates/dashboard.html:177
|
||||||
msgid "View"
|
msgid "View"
|
||||||
msgstr "Ver"
|
msgstr "Ver"
|
||||||
|
|
||||||
#: app/templates/dashboard.html:182
|
#: app/templates/dashboard.html:188
|
||||||
msgid "No upcoming reservations in the next 7 days"
|
msgid "No upcoming reservations in the next 7 days"
|
||||||
msgstr "No hay próximas reservas en los próximos 7 días"
|
msgstr "No hay próximas reservas en los próximos 7 días"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:3
|
||||||
|
#: app/templates/genetic_optimizer.html:70
|
||||||
|
msgid "AI Room Reservation Optimizer"
|
||||||
|
msgstr "Optimizador de Aulas con IA"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:73
|
||||||
|
msgid ""
|
||||||
|
"Use genetic algorithms to automatically optimize room assignments based "
|
||||||
|
"on capacity, availability, and student enrollment."
|
||||||
|
msgstr ""
|
||||||
|
"Usa algoritmos genéticos para optimizar automáticamente la asignación de "
|
||||||
|
"aulas según capacidad, disponibilidad y matrícula de estudiantes."
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:90
|
||||||
|
msgid "Algorithm Status"
|
||||||
|
msgstr "Estado del Algoritmo"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:95
|
||||||
|
msgid "System Status"
|
||||||
|
msgstr "Estado del Sistema"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:97
|
||||||
|
msgid "Available Classrooms"
|
||||||
|
msgstr "Aulas Disponibles"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:101
|
||||||
|
msgid "Active Commissions"
|
||||||
|
msgstr "Comisiones Activas"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:105
|
||||||
|
msgid "Existing Reservations"
|
||||||
|
msgstr "Reservas Existentes"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:111
|
||||||
|
msgid "Algorithm Configuration"
|
||||||
|
msgstr "Configuración del Algoritmo"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:113
|
||||||
|
msgid "Population Size"
|
||||||
|
msgstr "Tamaño de Población"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:117
|
||||||
|
msgid "Generations"
|
||||||
|
msgstr "Generaciones"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:121
|
||||||
|
msgid "Mutation Rate"
|
||||||
|
msgstr "Tasa de Mutación"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:128
|
||||||
|
msgid "Refresh Status"
|
||||||
|
msgstr "Actualizar Estado"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:140
|
||||||
|
msgid "Select Commissions to Optimize"
|
||||||
|
msgstr "Seleccionar Comisiones a Optimizar"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:144
|
||||||
|
msgid "Select All"
|
||||||
|
msgstr "Seleccionar Todas"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:147
|
||||||
|
msgid "Deselect All"
|
||||||
|
msgstr "Deseleccionar Todas"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:156
|
||||||
|
msgid "Loading commissions..."
|
||||||
|
msgstr "Cargando comisiones..."
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:171
|
||||||
|
msgid "Optimization Controls"
|
||||||
|
msgstr "Controles de Optimización"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:178
|
||||||
|
msgid "Start Date Range"
|
||||||
|
msgstr "Rango de Fecha de Inicio"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:184
|
||||||
|
msgid "End Date Range"
|
||||||
|
msgstr "Rango de Fecha de Fin"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:193
|
||||||
|
msgid "Preview Optimization"
|
||||||
|
msgstr "Previsualizar Optimización"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:197
|
||||||
|
msgid "Run Full Optimization"
|
||||||
|
msgstr "Ejecutar Optimización Completa"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:201
|
||||||
|
msgid "Apply Reservations"
|
||||||
|
msgstr "Aplicar Reservas"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:205
|
||||||
|
msgid "Reset"
|
||||||
|
msgstr "Restablecer"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:227
|
||||||
|
msgid "Running genetic algorithm optimization..."
|
||||||
|
msgstr "Ejecutando optimización por algoritmo genético..."
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:228
|
||||||
|
msgid "This may take a few moments"
|
||||||
|
msgstr "Esto puede demorar unos momentos"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:237
|
||||||
|
#: app/templates/schedule/calendar.html:59
|
||||||
|
msgid "Reservation Details"
|
||||||
|
msgstr "Detalles de la Reserva"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:244
|
||||||
|
#: app/templates/schedule/calendar.html:66
|
||||||
|
msgid "Close"
|
||||||
|
msgstr "Cerrar"
|
||||||
|
|
||||||
|
#: app/templates/genetic_optimizer.html:247
|
||||||
|
msgid "Download"
|
||||||
|
msgstr "Descargar"
|
||||||
|
|
||||||
|
#: app/templates/auth/login.html:6
|
||||||
|
msgid "Login - Edu-Space Admin"
|
||||||
|
msgstr "Iniciar Sesión - Edu-Space Admin"
|
||||||
|
|
||||||
|
#: app/templates/auth/login.html:51
|
||||||
|
msgid "Classroom Management System"
|
||||||
|
msgstr "Sistema de Gestión de Aulas"
|
||||||
|
|
||||||
|
#: app/templates/auth/login.html:73
|
||||||
|
msgid "Enter your email"
|
||||||
|
msgstr "Ingresa tu correo electrónico"
|
||||||
|
|
||||||
|
#: app/templates/auth/login.html:84
|
||||||
|
msgid "Enter your password"
|
||||||
|
msgstr "Ingresa tu contraseña"
|
||||||
|
|
||||||
|
#: app/templates/auth/login.html:106
|
||||||
|
msgid "Secure Login • All access is logged"
|
||||||
|
msgstr "Inicio de Sesión Seguro • Todos los accesos son registrados"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:3
|
||||||
|
msgid "User Profile"
|
||||||
|
msgstr "Perfil de Usuario"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:12
|
||||||
|
msgid "My Profile"
|
||||||
|
msgstr "Mi Perfil"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:33 app/templates/classrooms/list.html:73
|
||||||
|
msgid "Inactive"
|
||||||
|
msgstr "Inactiva"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:48
|
||||||
|
msgid "Full Name"
|
||||||
|
msgstr "Nombre Completo"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:52
|
||||||
|
msgid "Access Role"
|
||||||
|
msgstr "Rol de Acceso"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:56
|
||||||
|
msgid "Last Login"
|
||||||
|
msgstr "Último Inicio de Sesión"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:61
|
||||||
|
msgid "First login"
|
||||||
|
msgstr "Primer inicio de sesión"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:66
|
||||||
|
msgid "Registration Date"
|
||||||
|
msgstr "Fecha de Registro"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:82
|
||||||
|
msgid "Language Preference"
|
||||||
|
msgstr "Preferencia de Idioma"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:85
|
||||||
|
msgid ""
|
||||||
|
"If set, this language overrides the browser configuration for all your "
|
||||||
|
"sessions."
|
||||||
|
msgstr ""
|
||||||
|
"Si se configura, este idioma sobrescribe la configuración del navegador "
|
||||||
|
"en todas tus sesiones."
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:91
|
||||||
|
msgid "Auto-detect from browser (Default)"
|
||||||
|
msgstr "Detectar automáticamente del navegador (Por defecto)"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:103
|
||||||
|
msgid "Save Preference"
|
||||||
|
msgstr "Guardar Preferencia"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:110
|
||||||
|
msgid "Account Security"
|
||||||
|
msgstr "Seguridad de la cuenta"
|
||||||
|
|
||||||
|
#: app/templates/auth/profile.html:122
|
||||||
|
msgid "My Recent Reservations"
|
||||||
|
msgstr "Mis Reservas Recientes"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:14
|
||||||
|
msgid "Back to Classrooms"
|
||||||
|
msgstr "Volver a Aulas"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:25
|
||||||
|
msgid "Classroom Information"
|
||||||
|
msgstr "Información del Aula"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:86
|
||||||
|
msgid "Optional: Add any additional details about this classroom..."
|
||||||
|
msgstr "Opcional: Agrega detalles adicionales sobre esta aula..."
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:97
|
||||||
|
msgid "Preview"
|
||||||
|
msgstr "Vista Previa"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:100
|
||||||
|
msgid "Fill in the form to see a preview of the classroom code:"
|
||||||
|
msgstr "Completa el formulario para ver una vista previa del código del aula:"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:103
|
||||||
|
msgid "Full location:"
|
||||||
|
msgstr "Ubicación completa:"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:111
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "Cancelar"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:129
|
||||||
|
msgid "Resources (Optional)"
|
||||||
|
msgstr "Recursos (Opcional)"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/add.html:133
|
||||||
|
msgid "You can add classroom resources after creating the classroom."
|
||||||
|
msgstr "Puedes agregar recursos del aula después de crearla."
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:30
|
||||||
|
msgid "Search by building, room, description..."
|
||||||
|
msgstr "Buscar por edificio, aula, descripción..."
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:38
|
||||||
|
msgid "Show active classrooms only"
|
||||||
|
msgstr "Mostrar solo aulas activas"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:44
|
||||||
|
msgid "Search"
|
||||||
|
msgstr "Buscar"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:47
|
||||||
|
msgid "Clear"
|
||||||
|
msgstr "Limpiar"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:99
|
||||||
|
msgid "resources"
|
||||||
|
msgstr "recursos"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:101
|
||||||
|
msgid "No resources registered"
|
||||||
|
msgstr "Sin recursos registrados"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:116
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr "Editar"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:124
|
||||||
|
msgid "Deactivate"
|
||||||
|
msgstr "Desactivar"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:126
|
||||||
|
msgid "Activate"
|
||||||
|
msgstr "Activar"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:144
|
||||||
|
msgid "Previous"
|
||||||
|
msgstr "Anterior"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:172
|
||||||
|
msgid "Next"
|
||||||
|
msgstr "Siguiente"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:185
|
||||||
|
msgid "No classrooms found"
|
||||||
|
msgstr "No se encontraron aulas"
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:188
|
||||||
|
msgid "No classrooms match your search criteria."
|
||||||
|
msgstr "Ninguna aula coincide con tus criterios de búsqueda."
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:190
|
||||||
|
msgid "No classrooms have been registered yet."
|
||||||
|
msgstr "Aún no se han registrado aulas."
|
||||||
|
|
||||||
|
#: app/templates/classrooms/list.html:194
|
||||||
|
msgid "Add First Classroom"
|
||||||
|
msgstr "Agregar Primera Aula"
|
||||||
|
|
||||||
|
#: app/templates/schedule/calendar.html:3
|
||||||
|
#: app/templates/schedule/calendar.html:10
|
||||||
|
#: app/templates/schedule/calendar.html:21
|
||||||
|
msgid "Reservations Calendar"
|
||||||
|
msgstr "Calendario de Reservas"
|
||||||
|
|
||||||
|
#: app/templates/schedule/calendar.html:11
|
||||||
|
msgid "View & Manage Reservations"
|
||||||
|
msgstr "Ver y Gestionar Reservas"
|
||||||
|
|
||||||
|
#: app/templates/schedule/calendar.html:26
|
||||||
|
msgid "Add Reservation"
|
||||||
|
msgstr "Agregar Reserva"
|
||||||
|
|
||||||
|
#: app/templates/schedule/calendar.html:29
|
||||||
|
msgid "List View"
|
||||||
|
msgstr "Vista de Lista"
|
||||||
|
|
||||||
|
#: app/templates/schedule/calendar.html:40 app/templates/schedule/today.html:54
|
||||||
|
msgid "Confirmed"
|
||||||
|
msgstr "Confirmada"
|
||||||
|
|
||||||
|
#: app/templates/schedule/calendar.html:44 app/templates/schedule/today.html:70
|
||||||
|
msgid "Pending"
|
||||||
|
msgstr "Pendiente"
|
||||||
|
|
||||||
|
#: app/templates/schedule/today.html:38
|
||||||
|
msgid "Total Reservations"
|
||||||
|
msgstr "Total de Reservas"
|
||||||
|
|
||||||
|
#: app/templates/schedule/today.html:86
|
||||||
|
msgid "Active Classrooms"
|
||||||
|
msgstr "Aulas Ocupadas"
|
||||||
|
|
||||||
|
#: app/templates/schedule/today.html:342
|
||||||
|
msgid "No reservations scheduled for today"
|
||||||
|
msgstr "No hay reservas programadas para hoy"
|
||||||
|
|
||||||
|
#: app/templates/schedule/today.html:344
|
||||||
|
msgid "There are no classroom reservations for today."
|
||||||
|
msgstr "No hay reservas de aulas para hoy."
|
||||||
|
|
||||||
|
#: app/templates/schedule/today.html:348
|
||||||
|
msgid "Schedule First Class"
|
||||||
|
msgstr "Programar Primera Clase"
|
||||||
|
|
||||||
|
#: app/templates/schedule/today.html:351
|
||||||
|
msgid "View Week Schedule"
|
||||||
|
msgstr "Ver Horario Semanal"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user