feat(i18n & ux): auditoria, traduccion completa, correcciones ux y gestion de idioma en perfil

This commit is contained in:
Carlos Tello
2026-09-02 23:45:13 -03:00
parent 3d4e9992f8
commit d1d4be0428
31 changed files with 2356 additions and 488 deletions
+24
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
{
"python.defaultInterpreterPath": "${workspaceFolder}\\venv\\Scripts\\python.exe",
"python.analysis.extraPaths": [
"${workspaceFolder}\\venv\\Lib\\site-packages"
]
}
+84
View File
@@ -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`.
+443
View File
@@ -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')`.
+1
View File
@@ -3,4 +3,5 @@ from app import create_app
app = create_app()
if __name__ == '__main__':
# Server entrypoint with auto-reload (locale auto-detection & profile override)
app.run(debug=True, host='0.0.0.0', port=5000)
+22 -16
View File
@@ -41,28 +41,34 @@ def create_app(config_class=Config):
app.register_blueprint(genetic_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
def inject_conf_vars():
return dict(
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
babel.init_app(app, locale_selector=get_locale)
+5 -4
View File
@@ -1,13 +1,14 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, Length
from flask_babel import lazy_gettext as _l
class LoginForm(FlaskForm):
email = StringField('Email', validators=[
email = StringField(_l('Email'), validators=[
DataRequired(),
Email(),
Length(max=120)
])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember me')
submit = SubmitField('Sign In')
password = PasswordField(_l('Password'), validators=[DataRequired()])
remember_me = BooleanField(_l('Remember me'))
submit = SubmitField(_l('Sign In'))
+22 -21
View File
@@ -1,44 +1,45 @@
from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField, TextAreaField, BooleanField, SubmitField, SelectField
from wtforms.validators import DataRequired, Length, NumberRange, Optional
from flask_babel import lazy_gettext as _l
class ClassroomForm(FlaskForm):
# campus_id = SelectField('Campus', coerce=int, validators=[DataRequired()])
building = StringField('Building', validators=[
# campus_id = SelectField(_l('Campus'), coerce=int, validators=[DataRequired()])
building = StringField(_l('Building'), validators=[
DataRequired(),
Length(min=2, max=100)
])
room_number = StringField('Room Number', validators=[
room_number = StringField(_l('Room Number'), validators=[
DataRequired(),
Length(min=1, max=50)
])
floor = IntegerField('Floor', validators=[
floor = IntegerField(_l('Floor'), validators=[
DataRequired(),
NumberRange(min=1, max=50)
])
capacity = IntegerField('Capacity', validators=[
capacity = IntegerField(_l('Capacity'), validators=[
DataRequired(),
NumberRange(min=1, max=500)
])
description = TextAreaField('Description', validators=[
description = TextAreaField(_l('Description'), validators=[
Optional(),
Length(max=500)
])
is_active = BooleanField('Active', default=True)
submit = SubmitField('Save Classroom')
is_active = BooleanField(_l('Active'), default=True)
submit = SubmitField(_l('Save Classroom'))
class ClassroomResourceForm(FlaskForm):
resource_type = SelectField('Resource Type', choices=[
('PROJECTOR', 'Projector'),
('WHITEBOARD', 'Whiteboard'),
('COMPUTER', 'Computer'),
('SOUND_SYSTEM', 'Sound System'),
('MICROPHONE', 'Microphone'),
('AIR_CONDITIONER', 'Air Conditioner'),
('OTHER', 'Other')
resource_type = SelectField(_l('Resource Type'), choices=[
('PROJECTOR', _l('Projector')),
('WHITEBOARD', _l('Whiteboard')),
('COMPUTER', _l('Computer')),
('SOUND_SYSTEM', _l('Sound System')),
('MICROPHONE', _l('Microphone')),
('AIR_CONDITIONER', _l('Air Conditioner')),
('OTHER', _l('Other'))
], validators=[DataRequired()])
brand = StringField('Brand', validators=[Optional(), Length(max=100)])
model = StringField('Model', validators=[Optional(), Length(max=100)])
specifications = TextAreaField('Specifications', validators=[Optional()])
functional = BooleanField('Functional', default=True)
submit = SubmitField('Save Resource')
brand = StringField(_l('Brand'), validators=[Optional(), Length(max=100)])
model = StringField(_l('Model'), validators=[Optional(), Length(max=100)])
specifications = TextAreaField(_l('Specifications'), validators=[Optional()])
functional = BooleanField(_l('Functional'), default=True)
submit = SubmitField(_l('Save Resource'))
+9 -8
View File
@@ -1,26 +1,27 @@
from flask_wtf import FlaskForm
from wtforms import SelectField, StringField, TextAreaField, IntegerField, SubmitField, DateTimeLocalField
from wtforms.validators import DataRequired, Length, NumberRange, Optional, ValidationError
from flask_babel import lazy_gettext as _l
from datetime import datetime, timedelta
class ReservationForm(FlaskForm):
classroom_id = SelectField('Classroom', coerce=int, validators=[DataRequired()])
commission_id = SelectField('Commission/Subject', coerce=int, validators=[DataRequired()])
start_time = DateTimeLocalField('Start Time', format='%Y-%m-%dT%H:%M', validators=[DataRequired()])
end_time = DateTimeLocalField('End Time', format='%Y-%m-%dT%H:%M', validators=[DataRequired()])
purpose = StringField('Purpose', validators=[
classroom_id = SelectField(_l('Classroom'), coerce=int, validators=[DataRequired()])
commission_id = SelectField(_l('Commission/Subject'), coerce=int, validators=[DataRequired()])
start_time = DateTimeLocalField(_l('Start 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(_l('Purpose'), validators=[
DataRequired(),
Length(min=3, max=500)
])
expected_attendees = IntegerField('Expected Attendees', validators=[
expected_attendees = IntegerField(_l('Expected Attendees'), validators=[
DataRequired(),
NumberRange(min=1, max=500)
])
notes = TextAreaField('Notes', validators=[
notes = TextAreaField(_l('Notes'), validators=[
Optional(),
Length(max=1000)
])
submit = SubmitField('Make Reservation')
submit = SubmitField(_l('Make Reservation'))
def validate_end_time(self, field):
if field.data <= self.start_time.data:
+1
View File
@@ -14,6 +14,7 @@ class User(UserMixin, db.Model):
is_active = db.Column(db.Boolean, default=True, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime)
preferred_language = db.Column(db.String(10), default=None, nullable=True)
# Virtual fields for future compatibility
@property
+16 -1
View File
@@ -25,6 +25,10 @@ def login():
login_user(user, remember=form.remember_me.data)
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')
if not next_page or not next_page.startswith('/'):
next_page = url_for('main.dashboard')
@@ -41,15 +45,26 @@ def login():
def logout():
"""Handle user logout"""
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')
return redirect(url_for('auth.login'))
@auth_bp.route('/profile')
@auth_bp.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
"""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')
@auth_bp.route('/change_password', methods=['GET', 'POST'])
@login_required
def change_password():
+1
View File
@@ -52,6 +52,7 @@ def add_classroom():
return render_template('classrooms/add.html', form=form, title='Add Classroom')
@classrooms_bp.route('/', endpoint='index')
@classrooms_bp.route('/list')
@login_required
def list_classrooms():
+5
View File
@@ -1,5 +1,6 @@
from flask import Blueprint, render_template, jsonify, redirect, url_for, session, request, flash
from flask_login import login_required, current_user
from app import db
from app.models.reservation import Reservation
from app.models.classroom import Classroom
from app.models.subject import Subject
@@ -124,4 +125,8 @@ def set_language(language=None):
language = 'en'
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'))
+26
View File
@@ -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 %}
+62
View File
@@ -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 %}
+34 -20
View File
@@ -1,42 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ session.language or request.accept_languages.best_match(['es', 'en']) or 'es' }}">
<head>
<meta charset="UTF-8">
<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-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: system-ui, -apple-system, sans-serif;
}
.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);
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;
width: 100%;
color: #f8fafc;
}
.logo-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
border-radius: 20px 20px 0 0;
padding: 2rem;
text-align: center;
color: white;
position: relative;
}
.lang-switch {
position: absolute;
top: 1rem;
right: 1rem;
}
</style>
</head>
<body>
<div class="login-card">
<div class="logo-section">
<div class="lang-switch">
{% include 'partials/language_switcher.html' %}
</div>
<i class="bi bi-building" style="font-size: 3rem;"></i>
<h3 class="mt-3">Edu-Space Admin</h3>
<p class="mb-0">Classroom Management System</p>
<h3 class="mt-3 mb-1 fw-bold">Edu-Space Admin</h3>
<p class="mb-0 text-white-50 small">{% trans %}Classroom Management System{% endtrans %}</p>
</div>
<div class="p-4">
@@ -55,10 +67,10 @@
{{ form.hidden_tag() }}
<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">
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
{{ form.email(class="form-control", placeholder="Enter your email") }}
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-envelope"></i></span>
<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>
{% for error in form.email.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
@@ -66,10 +78,10 @@
</div>
<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">
<span class="input-group-text"><i class="bi bi-lock"></i></span>
{{ form.password(class="form-control", placeholder="Enter your password") }}
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-lock"></i></span>
<input type="password" class="form-control bg-dark border-secondary text-white" id="password" name="password" required placeholder="{% trans %}Enter your password{% endtrans %}">
</div>
{% for error in form.password.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
@@ -78,18 +90,20 @@
<div class="mb-3 form-check">
{{ 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 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>
</form>
<div class="text-center mt-3">
<small class="text-muted">
<i class="bi bi-shield-lock"></i>
Secure Login • All access is logged
<div class="text-center mt-4">
<small class="text-secondary">
<i class="bi bi-shield-lock me-1"></i>
{% trans %}Secure Login • All access is logged{% endtrans %}
</small>
</div>
</div>
+161
View File
@@ -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
View File
@@ -31,62 +31,43 @@
<ul class="navbar-nav me-auto">
<li class="nav-item">
<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>
</li>
<li class="nav-item 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>
<ul class="dropdown-menu">
<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>
<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>
</ul>
</li>
<li class="nav-item 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>
<ul class="dropdown-menu">
<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>
<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>
<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>
<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>
</ul>
</li>
</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">
<!-- Language Switcher -->
@@ -100,14 +81,14 @@
</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
<i class="bi bi-person"></i> {% trans %}Profile{% endtrans %}
</a></li>
<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>
<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
<i class="bi bi-box-arrow-right"></i> {% trans %}Logout{% endtrans %}
</a></li>
</ul>
</li>
+24 -21
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Add Classroom - Edu-Space Admin{% endblock %}
{% block title %}{% trans %}Add Classroom{% endtrans %} - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
@@ -8,10 +8,10 @@
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<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>
<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>
</div>
</div>
@@ -20,9 +20,9 @@
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0">
<h5 class="mb-0">
<i class="bi bi-door-open text-primary"></i> Classroom Information
<div class="card-header bg-body-tertiary border-0 py-3">
<h5 class="mb-0 h6 fw-bold">
<i class="bi bi-door-open text-primary me-2"></i> {% trans %}Classroom Information{% endtrans %}
</h5>
</div>
<div class="card-body">
@@ -73,16 +73,17 @@
</div>
<div class="col-md-6 mb-3">
{{ form.is_active.label(class="form-check-label") }}
<div class="form-check">
<label class="form-label d-block">&nbsp;</label>
<div class="form-check pt-2">
{{ form.is_active(class="form-check-input") }}
{{ form.is_active.label(class="form-check-label") }}
</div>
</div>
</div>
<div class="mb-4">
{{ 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 %}
<div class="text-danger small mt-1">
{{ form.description.errors[0] }}
@@ -93,13 +94,13 @@
<!-- Preview Section -->
<div class="alert alert-info">
<h6 class="alert-heading">
<i class="bi bi-info-circle"></i> Preview
<i class="bi bi-info-circle"></i> {% trans %}Preview{% endtrans %}
</h6>
<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>
</p>
<p class="mb-0">Full location:
<p class="mb-0">{% trans %}Full location:{% endtrans %}
<span class="fw-bold" id="preview-location">-</span>
</p>
</div>
@@ -107,9 +108,11 @@
<div class="d-flex justify-content-between">
<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>
{{ 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>
</form>
</div>
@@ -118,16 +121,16 @@
</div>
<!-- Resources Section -->
<div class="row mt-4">
<div class="row mt-4 mb-4">
<div class="col-lg-8 mx-auto">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0">
<h5 class="mb-0">
<i class="bi bi-tools text-warning"></i> Resources (Optional)
<div class="card-header bg-body-tertiary border-0 py-3">
<h5 class="mb-0 h6 fw-bold">
<i class="bi bi-tools text-warning me-2"></i> {% trans %}Resources (Optional){% endtrans %}
</h5>
</div>
<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>
@@ -142,10 +145,11 @@ $(document).ready(function() {
const building = $('#building').val();
const roomNumber = $('#room_number').val();
const floor = $('#floor').val();
const floorLabel = '{{ _("Floor") }}';
if (building && roomNumber && floor) {
$('#preview-code').text(`${building}-${roomNumber}`);
$('#preview-location').text(`${building} - Floor ${floor}`);
$('#preview-location').text(`${building} - ${floorLabel} ${floor}`);
} else {
$('#preview-code').text('-');
$('#preview-location').text('-');
@@ -162,7 +166,6 @@ $(document).ready(function() {
$('#classroom-form').on('submit', function(e) {
let isValid = true;
// Required fields validation
if (!$('#building').val().trim()) {
$('#building').addClass('is-invalid');
isValid = false;
+25 -25
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Classrooms - Edu-Space Admin{% endblock %}
{% block title %}{% trans %}Classrooms{% endtrans %} - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
@@ -8,11 +8,11 @@
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<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>
<div>
<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>
</div>
</div>
@@ -27,24 +27,24 @@
<form method="GET" class="row g-3">
<div class="col-md-4">
<input type="text" name="search" class="form-control"
placeholder="Search by building, room, description..."
placeholder="{% trans %}Search by building, room, description...{% endtrans %}"
value="{{ search }}">
</div>
<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"
id="activeOnly" {% if active_only %}checked{% endif %}>
<label class="form-check-label" for="activeOnly">
Show active classrooms only
{% trans %}Show active classrooms only{% endtrans %}
</label>
</div>
</div>
<div class="col-md-3">
<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>
<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>
</div>
</form>
@@ -63,24 +63,24 @@
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h5 class="card-title mb-1">
{{ classroom.code }}
{{ classroom.code_display }}
</h5>
<p class="text-muted small mb-2">
<i class="bi bi-geo-alt"></i> {{ classroom.get_full_location() }}
</p>
</div>
<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>
</div>
<div class="row small">
<div class="col-6">
<span class="text-muted">Capacity:</span>
<span class="text-muted">{% trans %}Capacity{% endtrans %}:</span>
<strong>{{ classroom.capacity }}</strong>
</div>
<div class="col-6">
<span class="text-muted">Building:</span>
<span class="text-muted">{% trans %}Building{% endtrans %}:</span>
<strong>{{ classroom.building }}</strong>
</div>
</div>
@@ -96,24 +96,24 @@
<small class="text-muted">
<i class="bi bi-tools"></i>
{% if classroom.resources %}
{{ classroom.resources|length }} resources
{{ classroom.resources|length }} {% trans %}resources{% endtrans %}
{% else %}
No resources registered
{% trans %}No resources registered{% endtrans %}
{% endif %}
</small>
</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>
<a href="{{ url_for('classrooms.view_classroom', id=classroom.id) }}"
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 href="{{ url_for('classrooms.edit_classroom', id=classroom.id) }}"
class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i> Edit
<i class="bi bi-pencil"></i> {% trans %}Edit{% endtrans %}
</a>
</div>
<form method="POST" action="{{ url_for('classrooms.toggle_classroom', id=classroom.id) }}"
@@ -121,9 +121,9 @@
<button type="submit" class="btn btn-sm
{% if classroom.is_active %}btn-outline-warning{% else %}btn-outline-success{% endif %}">
{% if classroom.is_active %}
<i class="bi bi-pause-circle"></i> Deactivate
<i class="bi bi-pause-circle"></i> {% trans %}Deactivate{% endtrans %}
{% else %}
<i class="bi bi-play-circle"></i> Activate
<i class="bi bi-play-circle"></i> {% trans %}Activate{% endtrans %}
{% endif %}
</button>
</form>
@@ -141,7 +141,7 @@
{% if classrooms.has_prev %}
<li class="page-item">
<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>
</li>
{% endif %}
@@ -169,7 +169,7 @@
{% if classrooms.has_next %}
<li class="page-item">
<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>
</li>
{% endif %}
@@ -182,16 +182,16 @@
<div class="col-12">
<div class="text-center py-5">
<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">
{% if search %}
No classrooms match your search criteria.
{% trans %}No classrooms match your search criteria.{% endtrans %}
{% else %}
No classrooms have been registered yet.
{% trans %}No classrooms have been registered yet.{% endtrans %}
{% endif %}
</p>
<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>
</div>
</div>
+18 -18
View File
@@ -88,7 +88,7 @@
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-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>
</div>
<div class="card-body">
@@ -102,28 +102,28 @@
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-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>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<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 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 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 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 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 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>
</div>
</div>
@@ -138,10 +138,10 @@
<div class="card-header bg-white border-0">
<div class="d-flex justify-content-between align-items-center">
<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>
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-sm btn-outline-primary">
View All
{% trans %}View All{% endtrans %}
</a>
</div>
</div>
@@ -151,12 +151,12 @@
<table class="table table-hover">
<thead>
<tr>
<th>Date & Time</th>
<th>Classroom</th>
<th>Subject</th>
<th>Teacher</th>
<th>Status</th>
<th>Actions</th>
<th>{% trans %}Date & Time{% endtrans %}</th>
<th>{% trans %}Classroom{% endtrans %}</th>
<th>{% trans %}Subject{% endtrans %}</th>
<th>{% trans %}Teacher{% endtrans %}</th>
<th>{% trans %}Status{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</tr>
</thead>
<tbody>
@@ -174,7 +174,7 @@
<td>
<a href="{{ url_for('schedule.list_reservations') }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> View
<i class="bi bi-eye"></i> {% trans %}View{% endtrans %}
</a>
</td>
</tr>
@@ -185,7 +185,7 @@
{% else %}
<div class="text-center py-4">
<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>
{% endif %}
</div>
+107 -189
View File
@@ -1,6 +1,6 @@
{% 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 %}
<style>
@@ -13,27 +13,24 @@
}
.commission-item {
border-left: 4px solid #007bff;
border-left: 4px solid var(--bs-primary);
transition: all 0.2s ease;
background-color: var(--bs-body-bg);
}
.commission-item:hover {
border-left-color: #0056b3;
background-color: #f8f9fa;
border-left-color: var(--bs-info);
background-color: rgba(var(--bs-primary-rgb), 0.05);
}
.commission-item.selected {
border-left-color: #28a745;
background-color: #d4edda;
border-left-color: var(--bs-success);
background-color: rgba(var(--bs-success-rgb), 0.1);
}
.algorithm-status {
position: sticky;
top: 20px;
}
.loading-spinner {
display: none;
top: 90px;
}
.spinner-overlay {
@@ -42,99 +39,93 @@
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
background-color: rgba(0, 0, 0, 0.7);
display: none;
justify-content: center;
align-items: center;
z-index: 9999;
}
.fitness-meter {
height: 20px;
background: linear-gradient(to right, #dc3545, #ffc107, #28a745);
border-radius: 10px;
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;
.hero-banner {
background: linear-gradient(135deg, rgba(37, 99, 235, 0.2) 0%, rgba(30, 41, 59, 0.9) 100%);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="container-fluid pt-5 mt-4">
<!-- Header -->
<div class="row mb-4">
<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">
<h1 class="card-title mb-3">
<i class="fas fa-brain me-2"></i>
AI Room Reservation Optimizer
<div class="d-flex align-items-center mb-2">
<div class="bg-primary bg-opacity-25 rounded-3 p-3 me-3 text-primary">
<i class="bi bi-cpu fs-2"></i>
</div>
<div>
<h1 class="h3 mb-1 fw-bold">
{% trans %}AI Room Reservation Optimizer{% endtrans %}
</h1>
<p class="card-text mb-0">
Use genetic algorithms to automatically optimize room assignments based on capacity, availability, and student enrollment.
<p class="text-muted mb-0">
{% trans %}Use genetic algorithms to automatically optimize room assignments based on capacity, availability, and student enrollment.{% endtrans %}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Algorithm Status Card -->
<!-- Algorithm Status & Commission Selection -->
<div class="row mb-4">
<div class="col-lg-4">
<div class="card algorithm-status">
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="fas fa-cogs me-2"></i>
Algorithm Status
<!-- Algorithm Status Card -->
<div class="col-lg-4 mb-4">
<div class="card border-0 shadow-sm algorithm-status">
<div class="card-header bg-body-tertiary border-0 py-3">
<h5 class="mb-0 h6 fw-bold">
<i class="bi bi-gear-wide-connected text-primary me-2"></i>
{% trans %}Algorithm Status{% endtrans %}
</h5>
</div>
<div class="card-body">
<div class="mb-3">
<small class="text-muted">System Status</small>
<div class="d-flex justify-content-between align-items-center">
<span>Available Classrooms</span>
<div class="mb-4">
<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 py-1 border-bottom border-body-secondary">
<span>{% trans %}Available Classrooms{% endtrans %}</span>
<span class="badge bg-success" id="available-classrooms">--</span>
</div>
<div class="d-flex justify-content-between align-items-center">
<span>Active Commissions</span>
<div class="d-flex justify-content-between align-items-center py-1 border-bottom border-body-secondary">
<span>{% trans %}Active Commissions{% endtrans %}</span>
<span class="badge bg-info" id="active-commissions">--</span>
</div>
<div class="d-flex justify-content-between align-items-center">
<span>Existing Reservations</span>
<div class="d-flex justify-content-between align-items-center py-1">
<span>{% trans %}Existing Reservations{% endtrans %}</span>
<span class="badge bg-secondary" id="existing-reservations">--</span>
</div>
</div>
<div class="mb-3">
<small class="text-muted">Algorithm Configuration</small>
<div class="d-flex justify-content-between align-items-center">
<span>Population Size</span>
<div class="mb-4">
<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 py-1 border-bottom border-body-secondary">
<span>{% trans %}Population Size{% endtrans %}</span>
<span class="text-muted">50</span>
</div>
<div class="d-flex justify-content-between align-items-center">
<span>Generations</span>
<div class="d-flex justify-content-between align-items-center py-1 border-bottom border-body-secondary">
<span>{% trans %}Generations{% endtrans %}</span>
<span class="text-muted">100</span>
</div>
<div class="d-flex justify-content-between align-items-center">
<span>Mutation Rate</span>
<div class="d-flex justify-content-between align-items-center py-1">
<span>{% trans %}Mutation Rate{% endtrans %}</span>
<span class="text-muted">10%</span>
</div>
</div>
<button class="btn btn-sm btn-outline-primary w-100" onclick="geneticManager.getAlgorithmStatus()">
<i class="fas fa-sync-alt me-1"></i>
Refresh Status
<i class="bi bi-arrow-clockwise me-1"></i>
{% trans %}Refresh Status{% endtrans %}
</button>
</div>
</div>
@@ -142,34 +133,27 @@
<!-- Commission Selection -->
<div class="col-lg-8">
<div class="card">
<div class="card-header bg-light">
<h5 class="mb-0">
<i class="fas fa-list-check me-2"></i>
Select Commissions to Optimize
<div class="card border-0 shadow-sm">
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 h6 fw-bold">
<i class="bi bi-list-check text-primary me-2"></i>
{% trans %}Select Commissions to Optimize{% endtrans %}
</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="d-flex gap-2 mb-2">
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-primary" onclick="selectAllCommissions()">
Select All
{% trans %}Select All{% endtrans %}
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="deselectAllCommissions()">
Deselect All
</button>
<button class="btn btn-sm btn-outline-info" onclick="filterCommissions()">
<i class="fas fa-filter me-1"></i>
Filter
{% trans %}Deselect All{% endtrans %}
</button>
</div>
</div>
<div class="card-body">
<div id="commissions-list" class="row">
<!-- Commissions will be loaded here -->
<div class="col-12 text-center text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>
Loading commissions...
<div class="col-12 text-center text-muted py-5">
<div class="spinner-border spinner-border-sm me-2 text-primary" role="status"></div>
{% trans %}Loading commissions...{% endtrans %}
</div>
</div>
</div>
@@ -180,45 +164,45 @@
<!-- Optimization Controls -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="fas fa-play-circle me-2"></i>
Optimization Controls
<div class="card border-0 shadow-sm">
<div class="card-header bg-body-tertiary border-0 py-3">
<h5 class="mb-0 h6 fw-bold">
<i class="bi bi-sliders text-primary me-2"></i>
{% trans %}Optimization Controls{% endtrans %}
</h5>
</div>
<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="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">
</div>
</div>
<div class="col-md-6">
<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">
</div>
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
<button class="btn btn-info" id="preview-optimization-btn">
<i class="fas fa-eye me-2"></i>
Preview Optimization
<button class="btn btn-info text-white" id="preview-optimization-btn">
<i class="bi bi-eye me-1"></i>
{% trans %}Preview Optimization{% endtrans %}
</button>
<button class="btn btn-success" id="optimize-assignments-btn">
<i class="fas fa-rocket me-2"></i>
Run Full Optimization
<i class="bi bi-play-circle me-1"></i>
{% trans %}Run Full Optimization{% endtrans %}
</button>
<button class="btn btn-warning" id="apply-optimization-btn" style="display: none;">
<i class="fas fa-check me-2"></i>
Apply Reservations
<i class="bi bi-check-circle me-1"></i>
{% trans %}Apply Reservations{% endtrans %}
</button>
<button class="btn btn-secondary" onclick="resetOptimization()">
<i class="fas fa-undo me-2"></i>
Reset
<button class="btn btn-outline-secondary" onclick="resetOptimization()">
<i class="bi bi-arrow-counterclockwise me-1"></i>
{% trans %}Reset{% endtrans %}
</button>
</div>
</div>
@@ -229,13 +213,8 @@
<!-- Results Section -->
<div class="row mb-4">
<div class="col-12">
<!-- Preview Results -->
<div id="preview-results"></div>
<!-- Optimization Results -->
<div id="optimization-results"></div>
<!-- Alert Container -->
<div id="alert-container"></div>
</div>
</div>
@@ -244,11 +223,9 @@
<!-- Loading Spinner Overlay -->
<div class="spinner-overlay" id="loading-spinner">
<div class="text-center text-white">
<div class="spinner-border" style="width: 3rem; height: 3rem;" role="status">
<span class="visually-hidden">Optimizing...</span>
</div>
<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 class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status"></div>
<p class="mt-3 mb-1 fw-bold fs-5">{% trans %}Running genetic algorithm optimization...{% endtrans %}</p>
<small class="text-white-50">{% trans %}This may take a few moments{% endtrans %}</small>
</div>
</div>
@@ -257,17 +234,17 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<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>
</div>
<div class="modal-body">
<!-- Content will be populated dynamically -->
<!-- Content populated dynamically -->
</div>
<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()">
<i class="fas fa-download me-2"></i>
Download
<i class="bi bi-download me-1"></i>
{% trans %}Download{% endtrans %}
</button>
</div>
</div>
@@ -278,7 +255,6 @@
{% block extra_js %}
<script src="{{ url_for('static', filename='js/genetic-algorithm.js') }}"></script>
<script>
// Additional utility functions for the genetic optimizer interface
function selectAllCommissions() {
const checkboxes = document.querySelectorAll('input[name="commission_ids"]');
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() {
deselectAllCommissions();
geneticManager.clearResults();
geneticManager.clearPreview();
if (window.geneticManager) {
geneticManager.clearResults && geneticManager.clearResults();
geneticManager.clearPreview && geneticManager.clearPreview();
}
document.getElementById('start-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() {
await loadCommissions();
if (window.geneticManager) {
await geneticManager.getAlgorithmStatus();
}
});
async function loadCommissions() {
try {
const response = await fetch('/api/genetic/commissions');
const data = await response.json();
const commissionsList = document.getElementById('commissions-list');
const isSpanish = '{{ session.get("language", "es") }}' === 'es';
if (data.success && data.commissions && data.commissions.length > 0) {
const commissionsHtml = data.commissions.map(commission => `
<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="form-check">
<input class="form-check-input" type="checkbox" name="commission_ids"
value="${commission.id}" id="commission-${commission.id}"
onchange="updateCommissionCard('${commission.id}', this.checked)">
<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>
<small class="text-muted">${commission.subject?.name || 'Unknown Subject'}</small>
<small class="text-muted">${commission.subject?.name || (isSpanish ? 'Materia Desconocida' : 'Unknown Subject')}</small>
<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>` : ''}
</label>
</div>
@@ -389,46 +332,21 @@ const response = await fetch('/api/genetic/commissions');
commissionsList.innerHTML = commissionsHtml;
} else {
commissionsList.innerHTML = `
<div class="col-12 text-center text-muted">
<i class="fas fa-info-circle me-2"></i>
No active commissions found.
<div class="col-12 text-center text-muted py-5">
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
${isSpanish ? 'No se encontraron comisiones activas.' : 'No active commissions found.'}
</div>
`;
}
} catch (error) {
console.error('Error loading commissions:', error);
document.getElementById('commissions-list').innerHTML = `
<div class="col-12 text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
Error loading commissions: ${error.message}
<div class="col-12 text-center text-danger py-4">
<i class="bi bi-exclamation-triangle fs-2 d-block mb-2"></i>
Error al cargar comisiones: ${error.message}
</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>
{% endblock %}
@@ -1,9 +1,9 @@
{% if config.LANGUAGES|length > 1 %}
<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">
<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') %}
Spanish
Español
{% else %}
English
{% endif %}
+27 -24
View File
@@ -1,31 +1,32 @@
{% extends "base.html" %}
{% block title %}Schedule Calendar - Edu-Space Admin{% endblock %}
{% block title %}{% trans %}Reservations Calendar{% endtrans %} - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<div class="row mb-4">
<div class="col-12">
<h1 class="h3 mb-0">
<i class="bi bi-calendar3 text-primary"></i> Schedule Calendar
<small class="text-muted ms-2">View & Manage Reservations</small>
<i class="bi bi-calendar3 text-primary"></i> {% trans %}Reservations Calendar{% endtrans %}
<small class="text-muted ms-2">{% trans %}View & Manage Reservations{% endtrans %}</small>
</h1>
</div>
</div>
<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="col-md-6">
<h5 class="mb-0">
<i class="bi bi-calendar-week text-info"></i> Reservations Calendar
<h5 class="mb-0 h6 fw-bold">
<i class="bi bi-calendar-week text-info me-2"></i> {% trans %}Reservations Calendar{% endtrans %}
</h5>
</div>
<div class="col-md-6 text-end">
<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 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>
</div>
</div>
@@ -36,11 +37,11 @@
<div class="d-flex gap-3 flex-wrap">
<div class="d-flex align-items-center">
<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 class="d-flex align-items-center">
<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>
@@ -55,14 +56,15 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<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>
</div>
<div class="modal-body" id="reservationDetails">
<!-- Content will be loaded dynamically -->
</div>
<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>
@@ -95,11 +97,14 @@
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.0/main.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.0/locales-all.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var currentLang = '{{ session.get("language", "es") }}';
var calendar = new FullCalendar.Calendar(calendarEl, {
locale: currentLang,
initialView: 'timeGridWeek',
headerToolbar: {
left: 'prev,next today',
@@ -115,16 +120,13 @@ document.addEventListener('DOMContentLoaded', function() {
slotMaxTime: '22:00:00',
height: 'auto',
eventClick: function(info) {
// Prevent default link behavior
if (info.event.url) {
// Load reservation details in modal instead of navigation
loadReservationDetails(info.event.url);
return false;
}
},
events: '{{ url_for("schedule.calendar_data") }}',
loading: function(bool) {
// Show loading indicator if needed
}
});
@@ -133,38 +135,39 @@ document.addEventListener('DOMContentLoaded', function() {
function loadReservationDetails(url) {
$.get(url)
.done(function(data) {
var isEs = currentLang === 'es';
var html = `
<div class="row">
<div class="col-md-6">
<h6 class="text-muted mb-2">Classroom</h6>
<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>
<h6 class="text-muted mb-2">${isEs ? 'Aula' : 'Classroom'}</h6>
<p class="fw-bold">${data.classroom.code} - ${data.classroom.name || ''}</p>
<p><i class="bi bi-building text-primary"></i> ${data.classroom.building} - ${isEs ? 'Piso' : 'Floor'} ${data.classroom.floor}</p>
</div>
<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><i class="bi bi-person text-info"></i> ${data.commission.teacher_name}</p>
</div>
</div>
<div class="row mt-3">
<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-success"></i> ${new Date(data.end_time).toLocaleString()}</p>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">Details</h6>
<p><i class="bi bi-people text-primary"></i> ${data.expected_attendees} attendees</p>
<h6 class="text-muted mb-2">${isEs ? 'Detalles' : 'Details'}</h6>
<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>
</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);
$('#reservationModal').modal('show');
})
.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');
});
}
+3 -3
View File
@@ -168,7 +168,7 @@
{% if subject.description %}
<div class="card-footer bg-light">
<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>
</div>
{% endif %}
@@ -180,8 +180,8 @@
<i class="bi bi-inbox fs-1 text-muted d-block mb-3"></i>
<h4 class="text-muted">No Subjects Found</h4>
<p class="text-muted">There are no active subjects in the system.</p>
<a href="{{ url_for('classrooms.index') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add New Subject
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-primary">
<i class="bi bi-door-open"></i> View Classrooms
</a>
</div>
</div>
+15 -17
View File
@@ -8,20 +8,19 @@
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<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>
</h1>
<div>
<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 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>
{# Access to Genetic Algorithm Optimizer - Only for admin/teacher roles #}
{% 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">
<i class="bi bi-cpu"></i> AI Optimizer
<a href="{{ url_for('genetic_algorithm_web.genetic_optimizer') }}" class="btn btn-info text-white ms-2">
<i class="bi bi-cpu"></i> {% trans %}AI Room Optimizer{% endtrans %}
</a>
{% endif %}
</div>
@@ -36,7 +35,7 @@
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<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>
</div>
<div class="bg-info bg-opacity-10 rounded-circle p-3">
@@ -52,7 +51,7 @@
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<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>
</div>
<div class="bg-success bg-opacity-10 rounded-circle p-3">
@@ -68,7 +67,7 @@
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<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>
</div>
<div class="bg-warning bg-opacity-10 rounded-circle p-3">
@@ -84,7 +83,7 @@
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<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>
</div>
<div class="bg-primary bg-opacity-10 rounded-circle p-3">
@@ -340,21 +339,20 @@
<div class="col-12">
<div class="text-center py-5">
<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">
There are no classroom reservations for {{ today.strftime('%B %d, %Y') }}.
{% trans %}There are no classroom reservations for today.{% endtrans %}
</p>
<div class="mt-3">
<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 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>
{# Access to Genetic Algorithm Optimizer - Only for admin/teacher roles #}
{% 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">
<i class="bi bi-cpu"></i> AI Room Optimizer
<a href="{{ url_for('genetic_algorithm_web.genetic_optimizer') }}" class="btn btn-info text-white ms-2">
<i class="bi bi-cpu"></i> {% trans %}AI Room Optimizer{% endtrans %}
</a>
{% endif %}
</div>
+2 -2
View File
@@ -1,3 +1,3 @@
[python: **.py]
[jinja2: **/templates/**.html]
[python: app/**.py]
[jinja2.ext.babel_extract: app/templates/**.html]
extensions=jinja2.ext.i18n
+3 -1
View File
@@ -1,5 +1,7 @@
import os
basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
class Config:
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'
@@ -18,4 +20,4 @@ class Config:
LANGUAGES = ['en', 'es']
BABEL_DEFAULT_LOCALE = 'en'
BABEL_DEFAULT_TIMEZONE = 'UTC'
BABEL_TRANSLATION_DIRECTORIES = 'translations'
BABEL_TRANSLATION_DIRECTORIES = os.path.join(basedir, 'translations')
+43
View File
@@ -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()
+586 -35
View File
@@ -3,27 +3,219 @@
msgid ""
msgstr ""
"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"
"Last-Translator: Admin <admin@edu-space.com>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\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"
"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
msgid "Edu-Space Admin"
msgstr "Edu-Space Admin"
#: app/templates/dashboard.html:3
msgid "Dashboard - Edu-Space Admin"
msgstr "Dashboard - Edu-Space Admin"
#: app/templates/auth/profile.html:15 app/templates/base.html:34
#: app/templates/dashboard.html:12
msgid "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
msgid "Overview"
msgstr "Overview"
@@ -52,63 +244,422 @@ msgstr "Monthly Reservations"
msgid "Quick Actions"
msgstr "Quick Actions"
#: app/templates/dashboard.html:111
msgid "Add Classroom"
msgstr "Add Classroom"
#: app/templates/dashboard.html:114
msgid "New Reservation"
msgstr "New Reservation"
msgid "Manage Commissions"
msgstr "Manage Commissions"
#: app/templates/dashboard.html:117
msgid "Today's Schedule"
msgstr "Today's Schedule"
#: app/templates/dashboard.html:123 app/templates/schedule/today.html:23
#: app/templates/schedule/today.html:355
msgid "AI Room Optimizer"
msgstr "AI Room Optimizer"
#: app/templates/dashboard.html:120
#: app/templates/dashboard.html:126
msgid "View Calendar"
msgstr "View Calendar"
#: app/templates/dashboard.html:135
#: app/templates/dashboard.html:141
msgid "Upcoming Reservations (Next 7 Days)"
msgstr "Upcoming Reservations (Next 7 Days)"
#: app/templates/dashboard.html:138
#: app/templates/dashboard.html:144
msgid "View All"
msgstr "View All"
#: app/templates/dashboard.html:148
#: 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
#: app/templates/auth/profile.html:132 app/templates/dashboard.html:154
msgid "Date & Time"
msgstr "Date & Time"
#: app/templates/dashboard.html:149
msgid "Classroom"
msgstr "Classroom"
#: app/templates/dashboard.html:150
#: app/templates/dashboard.html:156
msgid "Subject"
msgstr "Subject"
#: app/templates/dashboard.html:151
#: app/templates/dashboard.html:157
msgid "Teacher"
msgstr "Teacher"
#: app/templates/dashboard.html:152
#: app/templates/auth/profile.html:133 app/templates/dashboard.html:158
msgid "Status"
msgstr "Status"
#: app/templates/dashboard.html:153
#: app/templates/dashboard.html:159
msgid "Actions"
msgstr "Actions"
#: app/templates/dashboard.html:171
#: app/templates/classrooms/list.html:112 app/templates/dashboard.html:177
msgid "View"
msgstr "View"
#: app/templates/dashboard.html:182
#: app/templates/dashboard.html:188
msgid "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"
+552 -35
View File
@@ -3,27 +3,210 @@
msgid ""
msgstr ""
"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"
"Last-Translator: Admin <admin@edu-space.com>\n"
"Language: es\n"
"Language-Team: es <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\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"
"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
msgid "Edu-Space Admin"
msgstr "Admin Edu-Space"
#: app/templates/dashboard.html:3
msgid "Dashboard - Edu-Space Admin"
msgstr "Panel - Admin Edu-Space"
#: app/templates/auth/profile.html:15 app/templates/base.html:34
#: app/templates/dashboard.html:12
msgid "Dashboard"
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
msgid "Overview"
msgstr "Resumen"
@@ -52,63 +235,397 @@ msgstr "Reservas Mensuales"
msgid "Quick Actions"
msgstr "Acciones Rápidas"
#: app/templates/dashboard.html:111
msgid "Add Classroom"
msgstr "Agregar Aula"
#: app/templates/dashboard.html:114
msgid "New Reservation"
msgstr "Nueva Reserva"
msgid "Manage Commissions"
msgstr "Gestionar Comisiones"
#: app/templates/dashboard.html:117
msgid "Today's Schedule"
msgstr "Horario de Hoy"
#: app/templates/dashboard.html:123 app/templates/schedule/today.html:23
#: app/templates/schedule/today.html:355
msgid "AI Room Optimizer"
msgstr "Optimizador de Aulas IA"
#: app/templates/dashboard.html:120
#: app/templates/dashboard.html:126
msgid "View Calendar"
msgstr "Ver Calendario"
#: app/templates/dashboard.html:135
#: app/templates/dashboard.html:141
msgid "Upcoming Reservations (Next 7 Days)"
msgstr "Próximas Reservas (Próximos 7 Días)"
#: app/templates/dashboard.html:138
#: app/templates/dashboard.html:144
msgid "View All"
msgstr "Ver Todas"
#: app/templates/dashboard.html:148
#: 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
#: app/templates/auth/profile.html:132 app/templates/dashboard.html:154
msgid "Date & Time"
msgstr "Fecha y Hora"
#: app/templates/dashboard.html:149
msgid "Classroom"
msgstr "Aula"
#: app/templates/dashboard.html:150
#: app/templates/dashboard.html:156
msgid "Subject"
msgstr "Materia"
#: app/templates/dashboard.html:151
#: app/templates/dashboard.html:157
msgid "Teacher"
msgstr "Profesor"
#: app/templates/dashboard.html:152
#: app/templates/auth/profile.html:133 app/templates/dashboard.html:158
msgid "Status"
msgstr "Estado"
#: app/templates/dashboard.html:153
#: app/templates/dashboard.html:159
msgid "Actions"
msgstr "Acciones"
#: app/templates/dashboard.html:171
#: app/templates/classrooms/list.html:112 app/templates/dashboard.html:177
msgid "View"
msgstr "Ver"
#: app/templates/dashboard.html:182
#: app/templates/dashboard.html:188
msgid "No upcoming reservations in the next 7 days"
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"