fix contract

This commit is contained in:
Alejandro Vazquez
2026-04-09 22:31:33 -03:00
parent 18f1d3a8c3
commit 0e39bf28e8
14 changed files with 2594 additions and 11 deletions
+3 -2
View File
@@ -31,13 +31,14 @@ def create_app(config_class=Config):
# Import models to ensure they are registered
from app.models import user
# Register blueprints
from app.routes import auth_bp, classrooms_bp, main_bp, schedule_bp
# Register blueprints
from app.routes import auth_bp, classrooms_bp, main_bp, schedule_bp, genetic_bp
app.register_blueprint(auth_bp, url_prefix="/")
app.register_blueprint(classrooms_bp, url_prefix="/classrooms")
app.register_blueprint(main_bp, url_prefix="/")
app.register_blueprint(schedule_bp, url_prefix="/schedule")
app.register_blueprint(genetic_bp)
# Babel language selector
@app.context_processor
+15 -4
View File
@@ -1,6 +1,17 @@
from .user import User
from .classroom import Classroom
from .subject import Subject
from .reservation import Reservation
from .classroom import Classroom, ClassroomResource
from .subject import Subject, Commission
from .reservation import Reservation, ReservationStatus
from .genetic_algorithm import GeneticAlgorithm, ReservationOptimizer
__all__ = ['User', 'Classroom', 'Subject', 'Reservation']
__all__ = [
'User',
'Classroom',
'ClassroomResource',
'Subject',
'Commission',
'Reservation',
'ReservationStatus',
'GeneticAlgorithm',
'ReservationOptimizer'
]
+397
View File
@@ -0,0 +1,397 @@
import random
import copy
from datetime import datetime, timedelta
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass
from app import db
from app.models.classroom import Classroom
from app.models.reservation import Reservation, ReservationStatus
from app.models.subject import Commission
@dataclass
class ReservationRequest:
"""Represents a reservation request from the teaching committee"""
commission_id: int
expected_attendees: int
purpose: str
preferred_start_time: datetime
preferred_end_time: datetime
priority: int = 1 # 1=highest, 5=lowest
flexibility_hours: int = 2 # How flexible the start time can be
subject_requirements: Dict = None # Special requirements for the subject
def __post_init__(self):
if self.subject_requirements is None:
self.subject_requirements = {}
@dataclass
class Gene:
"""Represents a single reservation assignment"""
commission_id: int
classroom_id: int
start_time: datetime
end_time: datetime
expected_attendees: int
purpose: str
class Individual:
"""Represents a complete reservation schedule (chromosome)"""
def __init__(self, genes: List[Gene]):
self.genes = genes
self.fitness = 0.0
self.conflicts = []
def calculate_fitness(self, classrooms: Dict[int, Classroom], requests: Dict[int, ReservationRequest]) -> float:
"""Calculate fitness score based on multiple factors"""
score = 0.0
self.conflicts = []
for gene in self.genes:
classroom = classrooms.get(gene.classroom_id)
request = requests.get(gene.commission_id)
if not classroom or not request:
continue
# Factor 1: Capacity efficiency (40% weight)
capacity_score = self._calculate_capacity_score(classroom, gene.expected_attendees)
score += capacity_score * 0.4
# Factor 2: Time preference satisfaction (30% weight)
time_score = self._calculate_time_score(gene, request)
score += time_score * 0.3
# Factor 3: Conflict penalty (20% weight)
conflict_score = self._calculate_conflict_penalty(gene, self.genes)
score += conflict_score * 0.2
# Factor 4: Resource matching (10% weight)
resource_score = self._calculate_resource_score(classroom, request.subject_requirements)
score += resource_score * 0.1
# Store conflicts for debugging
if conflict_score < 0:
self.conflicts.append({
'gene': gene,
'conflict_type': 'time_overlap'
})
self.fitness = score
return score
def _calculate_capacity_score(self, classroom: Classroom, expected_attendees: int) -> float:
"""Calculate how well the classroom capacity matches the expected attendees"""
if classroom.capacity < expected_attendees:
return 0.0 # Overfilled classroom
# Calculate efficiency: closer capacity match = higher score
waste_ratio = (classroom.capacity - expected_attendees) / expected_attendees
if waste_ratio <= 0.1: # Less than 10% waste
return 1.0
elif waste_ratio <= 0.3: # Less than 30% waste
return 0.8
elif waste_ratio <= 0.5: # Less than 50% waste
return 0.6
else:
return 0.4
def _calculate_time_score(self, gene: Gene, request: ReservationRequest) -> float:
"""Calculate how well the assigned time matches preferences"""
# Calculate time difference from preferred time
time_diff = abs((gene.start_time - request.preferred_start_time).total_seconds() / 3600)
if time_diff <= 0.5: # Within 30 minutes
return 1.0
elif time_diff <= request.flexibility_hours: # Within flexible window
return 0.8 - (time_diff / request.flexibility_hours) * 0.3
else:
return max(0.0, 0.5 - (time_diff - request.flexibility_hours) * 0.1)
def _calculate_conflict_penalty(self, gene: Gene, all_genes: List[Gene]) -> float:
"""Check for time conflicts in the same classroom"""
conflicts = 0
for other in all_genes:
if gene != other and gene.classroom_id == other.classroom_id:
if self._times_overlap(gene.start_time, gene.end_time, other.start_time, other.end_time):
conflicts += 1
if conflicts > 0:
return -conflicts * 0.5 # Penalty for each conflict
return 1.0
def _calculate_resource_score(self, classroom: Classroom, requirements: Dict) -> float:
"""Check if classroom meets subject requirements"""
if not requirements:
return 1.0
# This would need to be implemented based on actual classroom resources
# For now, return a default score
return 0.8
def _times_overlap(self, start1: datetime, end1: datetime, start2: datetime, end2: datetime) -> bool:
"""Check if two time periods overlap"""
return start1 < end2 and end1 > start2
class GeneticAlgorithm:
"""Main genetic algorithm for room reservation optimization"""
def __init__(self, population_size: int = 50, generations: int = 100,
mutation_rate: float = 0.1, crossover_rate: float = 0.8):
self.population_size = population_size
self.generations = generations
self.mutation_rate = mutation_rate
self.crossover_rate = crossover_rate
def optimize_reservations(self, requests: List[ReservationRequest],
available_classrooms: List[Classroom],
start_date: datetime, end_date: datetime) -> Individual:
"""Run genetic algorithm to find optimal reservation schedule"""
classrooms_dict = {c.id: c for c in available_classrooms}
requests_dict = {r.commission_id: r for r in requests}
# Initialize population
population = self._initialize_population(requests, available_classrooms, start_date, end_date)
# Evolve population
for generation in range(self.generations):
# Calculate fitness for all individuals
for individual in population:
individual.calculate_fitness(classrooms_dict, requests_dict)
# Sort by fitness (best first)
population.sort(key=lambda x: x.fitness, reverse=True)
# Create new generation
new_population = []
# Elitism: keep best 10%
elite_size = int(self.population_size * 0.1)
new_population.extend(population[:elite_size])
# Crossover and mutation for remaining
while len(new_population) < self.population_size:
if random.random() < self.crossover_rate:
parent1 = self._tournament_selection(population)
parent2 = self._tournament_selection(population)
child1, child2 = self._crossover(parent1, parent2)
# Mutate children
if random.random() < self.mutation_rate:
child1 = self._mutate(child1, available_classrooms, start_date, end_date)
if random.random() < self.mutation_rate:
child2 = self._mutate(child2, available_classrooms, start_date, end_date)
new_population.extend([child1, child2])
else:
# Direct copy with potential mutation
selected = self._tournament_selection(population)
if random.random() < self.mutation_rate:
selected = self._mutate(selected, available_classrooms, start_date, end_date)
new_population.append(selected)
population = new_population[:self.population_size]
# Return best individual from final generation
population.sort(key=lambda x: x.fitness, reverse=True)
return population[0]
def _initialize_population(self, requests: List[ReservationRequest],
classrooms: List[Classroom],
start_date: datetime, end_date: datetime) -> List[Individual]:
"""Create initial population with random assignments"""
population = []
for _ in range(self.population_size):
genes = []
for request in requests:
# Random classroom selection (with capacity constraint)
suitable_classrooms = [c for c in classrooms if c.capacity >= request.expected_attendees]
if not suitable_classrooms:
continue
classroom = random.choice(suitable_classrooms)
# Random time slot (within flexible window)
time_offset = random.uniform(-request.flexibility_hours, request.flexibility_hours)
start_time = request.preferred_start_time + timedelta(hours=time_offset)
duration = request.preferred_end_time - request.preferred_start_time
end_time = start_time + duration
gene = Gene(
commission_id=request.commission_id,
classroom_id=classroom.id,
start_time=start_time,
end_time=end_time,
expected_attendees=request.expected_attendees,
purpose=request.purpose
)
genes.append(gene)
population.append(Individual(genes))
return population
def _tournament_selection(self, population: List[Individual], tournament_size: int = 3) -> Individual:
"""Select individual using tournament selection"""
tournament = random.sample(population, min(tournament_size, len(population)))
return max(tournament, key=lambda x: x.fitness)
def _crossover(self, parent1: Individual, parent2: Individual) -> Tuple[Individual, Individual]:
"""Perform crossover between two parents"""
# Simple one-point crossover
if len(parent1.genes) <= 1 or len(parent2.genes) <= 1:
return Individual(copy.deepcopy(parent1.genes)), Individual(copy.deepcopy(parent2.genes))
crossover_point = random.randint(1, min(len(parent1.genes), len(parent2.genes)) - 1)
child1_genes = copy.deepcopy(parent1.genes[:crossover_point] + parent2.genes[crossover_point:])
child2_genes = copy.deepcopy(parent2.genes[:crossover_point] + parent1.genes[crossover_point:])
return Individual(child1_genes), Individual(child2_genes)
def _mutate(self, individual: Individual, classrooms: List[Classroom],
start_date: datetime, end_date: datetime) -> Individual:
"""Apply mutation to an individual"""
if not individual.genes:
return individual
# Random gene mutation
gene_to_mutate = random.choice(individual.genes)
# Mutation types
mutation_type = random.choice(['classroom', 'time'])
if mutation_type == 'classroom':
# Change classroom
suitable_classrooms = [c for c in classrooms if c.capacity >= gene_to_mutate.expected_attendees]
if suitable_classrooms:
gene_to_mutate.classroom_id = random.choice(suitable_classrooms).id
elif mutation_type == 'time':
# Slightly adjust time
time_adjustment = timedelta(hours=random.uniform(-1, 1))
gene_to_mutate.start_time += time_adjustment
gene_to_mutate.end_time += time_adjustment
return individual
class ReservationOptimizer:
"""High-level interface for the reservation optimization system"""
def __init__(self):
self.ga = GeneticAlgorithm()
def optimize_schedule(self, commission_ids: List[int],
admin_user_id: int,
start_date: datetime = None,
end_date: datetime = None) -> Dict:
"""Optimize reservation schedule for given commissions"""
# Default date range if not provided
if not start_date:
start_date = datetime.utcnow()
if not end_date:
end_date = start_date + timedelta(days=30)
# Get commissions and create requests
commissions = Commission.query.filter(
Commission.id.in_(commission_ids),
Commission.active == True
).all()
requests = []
for commission in commissions:
# Calculate preferred time based on semester/year
preferred_time = self._calculate_preferred_time(commission)
request = ReservationRequest(
commission_id=commission.id,
expected_attendees=commission.max_students,
purpose=f"Regular class - {commission.subject.name if commission.subject else 'Unknown subject'}",
preferred_start_time=preferred_time['start'],
preferred_end_time=preferred_time['end'],
priority=1, # All regular classes have same priority
flexibility_hours=2
)
requests.append(request)
# Get available classrooms
classrooms = Classroom.query.filter_by(is_active=True).all()
# Run genetic algorithm
best_individual = self.ga.optimize_reservations(requests, classrooms, start_date, end_date)
# Convert to reservation format
optimized_reservations = []
for gene in best_individual.genes:
reservation_data = {
'commission_id': gene.commission_id,
'classroom_id': gene.classroom_id,
'user_id': admin_user_id,
'start_time': gene.start_time,
'end_time': gene.end_time,
'purpose': gene.purpose,
'expected_attendees': gene.expected_attendees,
'status': ReservationStatus.PENDING
}
optimized_reservations.append(reservation_data)
return {
'success': True,
'fitness_score': best_individual.fitness,
'reservations': optimized_reservations,
'conflicts': best_individual.conflicts,
'total_requests': len(requests),
'assigned_reservations': len(optimized_reservations)
}
def _calculate_preferred_time(self, commission: Commission) -> Dict:
"""Calculate preferred time slots for a commission"""
# This is a simplified version - would need to be based on actual semester schedule
current_date = datetime.utcnow()
# Default to weekday mornings for regular classes
days_ahead = (0 - current_date.weekday()) % 7 # Next Monday
if days_ahead == 0: # If today is Monday, use next week
days_ahead = 7
preferred_date = current_date + timedelta(days=days_ahead)
return {
'start': preferred_date.replace(hour=9, minute=0, second=0, microsecond=0),
'end': preferred_date.replace(hour=11, minute=0, second=0, microsecond=0)
}
def apply_optimized_reservations(self, reservation_data: List[Dict]) -> List[Reservation]:
"""Apply the optimized reservations to the database"""
applied_reservations = []
for data in reservation_data:
# Check for conflicts before creating
conflicts = Reservation.find_conflicts(
data['classroom_id'],
data['start_time'],
data['end_time']
)
if not conflicts:
reservation = Reservation(**data)
db.session.add(reservation)
applied_reservations.append(reservation)
else:
# Log conflict - could be handled differently
print(f"Conflict detected for reservation data: {data}")
try:
db.session.commit()
except Exception as e:
db.session.rollback()
raise e
return applied_reservations
+4 -3
View File
@@ -1,6 +1,7 @@
from .auth import auth_bp
from .classrooms import classrooms_bp
from .main import main_bp
from .auth import auth_bp
from .classrooms import classrooms_bp
from .schedule import schedule_bp
from .genetic_algorithm import genetic_bp
__all__ = ['auth_bp', 'classrooms_bp', 'main_bp', 'schedule_bp']
__all__ = ['main_bp', 'auth_bp', 'classrooms_bp', 'schedule_bp', 'genetic_bp']
+241
View File
@@ -0,0 +1,241 @@
from flask import Blueprint, request, jsonify, current_app
from datetime import datetime, timedelta
from app.models.genetic_algorithm import ReservationOptimizer
from app.models.reservation import Reservation, ReservationStatus
from app import db
from flask_login import login_required, current_user
genetic_bp = Blueprint('genetic_algorithm', __name__, url_prefix='/api/genetic')
@genetic_bp.route('/optimize', methods=['POST'])
@login_required
def optimize_reservations():
"""Optimize reservations using genetic algorithm"""
try:
data = request.get_json()
if not data or 'commission_ids' not in data:
return jsonify({'error': 'Missing commission_ids in request'}), 400
commission_ids = data['commission_ids']
if not isinstance(commission_ids, list) or not commission_ids:
return jsonify({'error': 'commission_ids must be a non-empty list'}), 400
# Optional parameters
start_date = None
end_date = None
if 'start_date' in data:
try:
start_date = datetime.fromisoformat(data['start_date'])
except ValueError:
return jsonify({'error': 'Invalid start_date format. Use ISO format.'}), 400
if 'end_date' in data:
try:
end_date = datetime.fromisoformat(data['end_date'])
except ValueError:
return jsonify({'error': 'Invalid end_date format. Use ISO format.'}), 400
# Check if user has permission (admin or teacher)
if current_user.role not in ['admin', 'teacher']:
return jsonify({'error': 'Insufficient permissions to optimize reservations'}), 403
# Initialize optimizer and run optimization
optimizer = ReservationOptimizer()
result = optimizer.optimize_schedule(
commission_ids=commission_ids,
admin_user_id=current_user.id,
start_date=start_date,
end_date=end_date
)
return jsonify({
'success': True,
'data': {
'fitness_score': result['fitness_score'],
'total_requests': result['total_requests'],
'assigned_reservations': result['assigned_reservations'],
'reservations': result['reservations'],
'conflicts': result['conflicts']
}
}), 200
except Exception as e:
current_app.logger.error(f"Error in genetic algorithm optimization: {str(e)}")
return jsonify({'error': f'Optimization failed: {str(e)}'}), 500
@genetic_bp.route('/apply-optimization', methods=['POST'])
@login_required
def apply_optimization():
"""Apply optimized reservations to the database"""
try:
data = request.get_json()
if not data or 'reservations' not in data:
return jsonify({'error': 'Missing reservations data'}), 400
reservations_data = data['reservations']
if not isinstance(reservations_data, list):
return jsonify({'error': 'reservations must be a list'}), 400
# Check if user has permission
if current_user.role not in ['admin', 'teacher']:
return jsonify({'error': 'Insufficient permissions to apply reservations'}), 403
# Initialize optimizer and apply reservations
optimizer = ReservationOptimizer()
applied_reservations = optimizer.apply_optimized_reservations(reservations_data)
return jsonify({
'success': True,
'data': {
'applied_count': len(applied_reservations),
'reservations': [res.to_dict() for res in applied_reservations]
}
}), 200
except Exception as e:
current_app.logger.error(f"Error applying optimized reservations: {str(e)}")
db.session.rollback()
return jsonify({'error': f'Failed to apply reservations: {str(e)}'}), 500
@genetic_bp.route('/preview-optimization', methods=['POST'])
@login_required
def preview_optimization():
"""Preview optimization without applying to database"""
try:
data = request.get_json()
if not data or 'commission_ids' not in data:
return jsonify({'error': 'Missing commission_ids in request'}), 400
commission_ids = data['commission_ids']
# Optional parameters
dry_run = data.get('dry_run', True) # Default to dry run
if not isinstance(commission_ids, list) or not commission_ids:
return jsonify({'error': 'commission_ids must be a non-empty list'}), 400
# Check if user has permission
if current_user.role not in ['admin', 'teacher']:
return jsonify({'error': 'Insufficient permissions to preview optimization'}), 403
# Initialize optimizer and run optimization
optimizer = ReservationOptimizer()
result = optimizer.optimize_schedule(
commission_ids=commission_ids,
admin_user_id=current_user.id
)
# Add additional preview information
preview_data = {
'optimization_result': result,
'commission_details': [],
'classroom_utilization': {},
'time_distribution': {}
}
# Get commission details
from app.models.subject import Commission, Subject
commissions = Commission.query.filter(Commission.id.in_(commission_ids)).all()
for commission in commissions:
preview_data['commission_details'].append({
'id': commission.id,
'code': commission.get_full_code(),
'name': commission.subject.name if commission.subject else 'Unknown',
'max_students': commission.max_students,
'current_students': commission.current_students
})
return jsonify({
'success': True,
'data': preview_data
}), 200
except Exception as e:
current_app.logger.error(f"Error in optimization preview: {str(e)}")
return jsonify({'error': f'Preview failed: {str(e)}'}), 500
@genetic_bp.route('/commissions', methods=['GET'])
@login_required
def get_commissions():
"""Get available commissions for optimization"""
try:
# Check if user has permission
if current_user.role not in ['admin', 'teacher']:
return jsonify({'error': 'Insufficient permissions'}), 403
from app.models.subject import Commission, Subject
# Get active commissions
commissions = Commission.query.filter_by(active=True).all()
commissions_data = []
for commission in commissions:
commission_dict = commission.to_dict()
commission_dict['get_full_code'] = commission.get_full_code()
commissions_data.append(commission_dict)
return jsonify({
'success': True,
'commissions': commissions_data
}), 200
except Exception as e:
current_app.logger.error(f"Error getting commissions: {str(e)}")
return jsonify({'error': f'Failed to load commissions: {str(e)}'}), 500
@genetic_bp.route('/algorithm-status', methods=['GET'])
@login_required
def get_algorithm_status():
"""Get genetic algorithm configuration status"""
try:
# Check if user has permission
if current_user.role not in ['admin', 'teacher']:
return jsonify({'error': 'Insufficient permissions'}), 403
# Get system status
from app.models.classroom import Classroom
from app.models.subject import Commission
total_classrooms = Classroom.query.filter_by(is_active=True).count()
total_commissions = Commission.query.filter_by(active=True).count()
total_reservations = Reservation.query.filter_by(status=ReservationStatus.CONFIRMED).count()
return jsonify({
'success': True,
'data': {
'algorithm_config': {
'population_size': 50,
'generations': 100,
'mutation_rate': 0.1,
'crossover_rate': 0.8
},
'system_status': {
'available_classrooms': total_classrooms,
'active_commissions': total_commissions,
'confirmed_reservations': total_reservations
},
'capabilities': [
'Automatic classroom assignment',
'Capacity optimization',
'Time conflict resolution',
'Resource matching',
'Batch reservation processing'
]
}
}), 200
except Exception as e:
current_app.logger.error(f"Error getting algorithm status: {str(e)}")
return jsonify({'error': f'Status check failed: {str(e)}'}), 500
+10
View File
@@ -108,6 +108,16 @@ def dashboard_stats_api():
'monthly_data': list(reversed(monthly_data))
})
@main_bp.route('/genetic-optimizer')
@login_required
def genetic_optimizer():
"""Serve the genetic algorithm optimization interface"""
if current_user.role not in ['admin', 'teacher']:
flash('You do not have permission to access the optimization tool.', 'danger')
return redirect(url_for('main.dashboard'))
return render_template('genetic_optimizer.html')
@main_bp.route('/set_language/<language>')
def set_language(language=None):
if language not in ['en', 'es']:
+491
View File
@@ -0,0 +1,491 @@
class GeneticAlgorithmManager {
constructor() {
this.apiBaseUrl = '/api/genetic';
this.setupEventListeners();
}
setupEventListeners() {
// Initialize any UI elements if needed
document.addEventListener('DOMContentLoaded', () => {
this.initializeUI();
});
}
initializeUI() {
// Set up event listeners for genetic algorithm buttons
const optimizeBtn = document.getElementById('optimize-assignments-btn');
const previewBtn = document.getElementById('preview-optimization-btn');
const applyBtn = document.getElementById('apply-optimization-btn');
if (optimizeBtn) {
optimizeBtn.addEventListener('click', () => this.handleOptimizeReservations());
}
if (previewBtn) {
previewBtn.addEventListener('click', () => this.handlePreviewOptimization());
}
if (applyBtn) {
applyBtn.addEventListener('click', () => this.handleApplyOptimization());
}
}
async optimizeReservations(commissionIds, startDate = null, endDate = null) {
try {
const requestBody = {
commission_ids: commissionIds
};
if (startDate) {
requestBody.start_date = startDate;
}
if (endDate) {
requestBody.end_date = endDate;
}
const response = await fetch(`${this.apiBaseUrl}/optimize`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCSRFToken()
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Optimization failed');
}
return await response.json();
} catch (error) {
console.error('Error optimizing reservations:', error);
throw error;
}
}
async previewOptimization(commissionIds) {
try {
const response = await fetch(`${this.apiBaseUrl}/preview-optimization`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCSRFToken()
},
body: JSON.stringify({
commission_ids: commissionIds,
dry_run: true
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Preview failed');
}
return await response.json();
} catch (error) {
console.error('Error previewing optimization:', error);
throw error;
}
}
async applyOptimization(reservations) {
try {
const response = await fetch(`${this.apiBaseUrl}/apply-optimization`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCSRFToken()
},
body: JSON.stringify({
reservations: reservations
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to apply optimization');
}
return await response.json();
} catch (error) {
console.error('Error applying optimization:', error);
throw error;
}
}
async getAlgorithmStatus() {
try {
const response = await fetch(`${this.apiBaseUrl}/algorithm-status`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCSRFToken()
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to get algorithm status');
}
return await response.json();
} catch (error) {
console.error('Error getting algorithm status:', error);
throw error;
}
}
async handleOptimizeReservations() {
try {
// Get selected commissions from UI
const commissionIds = this.getSelectedCommissionIds();
if (commissionIds.length === 0) {
this.showAlert('Please select at least one commission to optimize.', 'warning');
return;
}
this.showLoading(true);
const result = await this.optimizeReservations(commissionIds);
if (result.success) {
this.displayOptimizationResults(result.data);
this.showAlert('Optimization completed successfully!', 'success');
} else {
this.showAlert('Optimization failed: ' + (result.error || 'Unknown error'), 'danger');
}
} catch (error) {
this.showAlert('Error during optimization: ' + error.message, 'danger');
} finally {
this.showLoading(false);
}
}
async handlePreviewOptimization() {
try {
const commissionIds = this.getSelectedCommissionIds();
if (commissionIds.length === 0) {
this.showAlert('Please select at least one commission to preview.', 'warning');
return;
}
this.showLoading(true);
const result = await this.previewOptimization(commissionIds);
if (result.success) {
this.displayPreviewResults(result.data);
this.showAlert('Preview generated successfully!', 'success');
} else {
this.showAlert('Preview failed: ' + (result.error || 'Unknown error'), 'danger');
}
} catch (error) {
this.showAlert('Error during preview: ' + error.message, 'danger');
} finally {
this.showLoading(false);
}
}
async handleApplyOptimization() {
try {
const pendingReservations = this.getPendingReservations();
if (!pendingReservations || pendingReservations.length === 0) {
this.showAlert('No pending reservations to apply.', 'warning');
return;
}
if (!confirm('Are you sure you want to apply these reservations? This will create actual reservation records.')) {
return;
}
this.showLoading(true);
const result = await this.applyOptimization(pendingReservations);
if (result.success) {
this.showAlert(`Successfully applied ${result.data.applied_count} reservations!`, 'success');
this.clearPendingReservations();
// Redirect to reservations page
window.location.href = '/schedule';
} else {
this.showAlert('Failed to apply reservations: ' + (result.error || 'Unknown error'), 'danger');
}
} catch (error) {
this.showAlert('Error applying reservations: ' + error.message, 'danger');
} finally {
this.showLoading(false);
}
}
getSelectedCommissionIds() {
const checkboxes = document.querySelectorAll('input[name="commission_ids"]:checked');
return Array.from(checkboxes).map(cb => parseInt(cb.value));
}
getPendingReservations() {
// Try to get from localStorage or a hidden form field
const stored = localStorage.getItem('pending_optimizations');
return stored ? JSON.parse(stored) : null;
}
storePendingReservations(reservations) {
localStorage.setItem('pending_optimizations', JSON.stringify(reservations));
}
clearPendingReservations() {
localStorage.removeItem('pending_optimizations');
}
displayOptimizationResults(data) {
// Store the results for potential application
this.storePendingReservations(data.reservations);
// Create results display
const resultsContainer = document.getElementById('optimization-results');
if (!resultsContainer) return;
const scorePercentage = (data.fitness_score * 100).toFixed(1);
resultsContainer.innerHTML = `
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0">Optimization Results</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-4">
<div class="text-center">
<h4 class="text-primary">${data.assigned_reservations}</h4>
<p class="mb-0">Reservations Assigned</p>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<h4 class="text-info">${scorePercentage}%</h4>
<p class="mb-0">Fitness Score</p>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<h4 class="text-warning">${data.total_requests}</h4>
<p class="mb-0">Total Requests</p>
</div>
</div>
</div>
${data.conflicts.length > 0 ? `
<div class="alert alert-warning">
<strong>Warning:</strong> ${data.conflicts.length} potential conflicts detected.
</div>
` : ''}
<div class="d-flex gap-2">
<button class="btn btn-primary" onclick="geneticManager.viewReservationDetails()">
View Details
</button>
<button class="btn btn-success" onclick="geneticManager.handleApplyOptimization()">
Apply Reservations
</button>
<button class="btn btn-secondary" onclick="geneticManager.clearResults()">
Clear
</button>
</div>
</div>
</div>
`;
}
displayPreviewResults(data) {
const previewContainer = document.getElementById('preview-results');
if (!previewContainer) return;
const commissionsHtml = data.commission_details.map(commission => `
<tr>
<td>${commission.code}</td>
<td>${commission.name}</td>
<td>${commission.current_students}/${commission.max_students}</td>
</tr>
`).join('');
const optimization = data.optimization_result;
previewContainer.innerHTML = `
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0">Optimization Preview</h5>
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col-md-6">
<h6>Commissions to Optimize</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Students</th>
</tr>
</thead>
<tbody>
${commissionsHtml}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h6>Expected Results</h6>
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between">
<span>Reservations Created:</span>
<strong>${optimization.assigned_reservations}</strong>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Fitness Score:</span>
<strong>${(optimization.fitness_score * 100).toFixed(1)}%</strong>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Conflicts:</span>
<strong class="${optimization.conflicts.length > 0 ? 'text-danger' : 'text-success'}">
${optimization.conflicts.length}
</strong>
</li>
</ul>
</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-primary" onclick="geneticManager.handleOptimizeReservations()">
Run Full Optimization
</button>
<button class="btn btn-secondary" onclick="geneticManager.clearPreview()">
Close Preview
</button>
</div>
</div>
</div>
`;
}
viewReservationDetails() {
const reservations = this.getPendingReservations();
if (!reservations || reservations.length === 0) {
this.showAlert('No reservation details available.', 'info');
return;
}
// Create modal or redirect to detailed view
const detailsHtml = reservations.map((res, index) => `
<tr>
<td>${index + 1}</td>
<td>${res.purpose}</td>
<td>Classroom ${res.classroom_id}</td>
<td>${new Date(res.start_time).toLocaleString()}</td>
<td>${new Date(res.end_time).toLocaleString()}</td>
<td>${res.expected_attendees}</td>
</tr>
`).join('');
const modal = document.getElementById('detailsModal');
if (modal) {
modal.querySelector('.modal-body').innerHTML = `
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Purpose</th>
<th>Classroom</th>
<th>Start Time</th>
<th>End Time</th>
<th>Attendees</th>
</tr>
</thead>
<tbody>
${detailsHtml}
</tbody>
</table>
</div>
`;
const bootstrapModal = new bootstrap.Modal(modal);
bootstrapModal.show();
}
}
clearResults() {
const resultsContainer = document.getElementById('optimization-results');
if (resultsContainer) {
resultsContainer.innerHTML = '';
}
this.clearPendingReservations();
}
clearPreview() {
const previewContainer = document.getElementById('preview-results');
if (previewContainer) {
previewContainer.innerHTML = '';
}
}
showLoading(show) {
const loadingSpinner = document.getElementById('loading-spinner');
if (loadingSpinner) {
loadingSpinner.style.display = show ? 'block' : 'none';
}
}
showAlert(message, type = 'info') {
// Create and show alert
const alertContainer = document.getElementById('alert-container');
if (!alertContainer) return;
const alertHtml = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
alertContainer.insertAdjacentHTML('beforeend', alertHtml);
// Auto-dismiss after 5 seconds
setTimeout(() => {
const alert = alertContainer.lastElementChild;
if (alert) {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
}
}, 5000);
}
getCSRFToken() {
// Get CSRF token from meta tag or cookie
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) {
return metaTag.getAttribute('content');
}
// Fallback to cookie
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'csrf_token') {
return decodeURIComponent(value);
}
}
return '';
}
}
// Initialize the genetic algorithm manager
const geneticManager = new GeneticAlgorithmManager();
// Export for global access
window.geneticManager = geneticManager;
+6
View File
@@ -119,6 +119,12 @@
<a href="{{ url_for('schedule.today_schedule') }}" class="btn btn-outline-info">
<i class="bi bi-calendar-today"></i> Today's Schedule
</a>
{# Access to Genetic Algorithm Optimizer - Only for admin/teacher roles #}
{% if current_user and current_user.role in ['ADMIN'] %}
<a href="{{ url_for('genetic_algorithm.genetic_optimizer') }}" class="btn btn-info text-white">
<i class="bi bi-cpu"></i> AI Room Optimizer
</a>
{% endif %}
<a href="{{ url_for('schedule.calendar_view') }}" class="btn btn-outline-secondary">
<i class="bi bi-calendar3"></i> View Calendar
</a>
+434
View File
@@ -0,0 +1,434 @@
{% extends "base.html" %}
{% block title %}AI Room Reservation Optimizer{% endblock %}
{% block extra_css %}
<style>
.optimization-card {
transition: transform 0.2s ease-in-out;
}
.optimization-card:hover {
transform: translateY(-2px);
}
.commission-item {
border-left: 4px solid #007bff;
transition: all 0.2s ease;
}
.commission-item:hover {
border-left-color: #0056b3;
background-color: #f8f9fa;
}
.commission-item.selected {
border-left-color: #28a745;
background-color: #d4edda;
}
.algorithm-status {
position: sticky;
top: 20px;
}
.loading-spinner {
display: none;
}
.spinner-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
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;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<div class="card bg-primary text-white">
<div class="card-body">
<h1 class="card-title mb-3">
<i class="fas fa-brain me-2"></i>
AI Room Reservation Optimizer
</h1>
<p class="card-text mb-0">
Use genetic algorithms to automatically optimize room assignments based on capacity, availability, and student enrollment.
</p>
</div>
</div>
</div>
</div>
<!-- Algorithm Status Card -->
<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
</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>
<span class="badge bg-success" id="available-classrooms">--</span>
</div>
<div class="d-flex justify-content-between align-items-center">
<span>Active Commissions</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>
<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>
<span class="text-muted">50</span>
</div>
<div class="d-flex justify-content-between align-items-center">
<span>Generations</span>
<span class="text-muted">100</span>
</div>
<div class="d-flex justify-content-between align-items-center">
<span>Mutation Rate</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
</button>
</div>
</div>
</div>
<!-- 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
</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="d-flex gap-2 mb-2">
<button class="btn btn-sm btn-outline-primary" onclick="selectAllCommissions()">
Select All
</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
</button>
</div>
</div>
<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>
</div>
</div>
</div>
</div>
</div>
<!-- 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
</h5>
</div>
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-6">
<div class="mb-3">
<label for="start-date" class="form-label">Start Date Range</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>
<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>
<button class="btn btn-success" id="optimize-assignments-btn">
<i class="fas fa-rocket me-2"></i>
Run Full Optimization
</button>
<button class="btn btn-warning" id="apply-optimization-btn" style="display: none;">
<i class="fas fa-check me-2"></i>
Apply Reservations
</button>
<button class="btn btn-secondary" onclick="resetOptimization()">
<i class="fas fa-undo me-2"></i>
Reset
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 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>
</div>
<!-- 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>
</div>
<!-- Details Modal -->
<div class="modal fade" id="detailsModal" tabindex="-1" aria-labelledby="detailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="detailsModalLabel">Reservation Details</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 -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="downloadReservations()">
<i class="fas fa-download me-2"></i>
Download
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% 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 => {
cb.checked = true;
updateCommissionCard(cb.value, true);
});
}
function deselectAllCommissions() {
const checkboxes = document.querySelectorAll('input[name="commission_ids"]');
checkboxes.forEach(cb => {
cb.checked = false;
updateCommissionCard(cb.value, false);
});
}
function updateCommissionCard(commissionId, selected) {
const card = document.querySelector(`[data-commission-id="${commissionId}"]`);
if (card) {
if (selected) {
card.classList.add('selected');
} else {
card.classList.remove('selected');
}
}
}
function filterCommissions() {
// Implement filtering logic
console.log('Filter commissions feature not implemented yet');
}
function resetOptimization() {
deselectAllCommissions();
geneticManager.clearResults();
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();
await geneticManager.getAlgorithmStatus();
});
async function loadCommissions() {
try {
const response = await fetch('/api/commissions');
const data = await response.json();
const commissionsList = document.getElementById('commissions-list');
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-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>
<br>
<small class="text-muted">${commission.subject?.name || 'Unknown Subject'}</small>
<br>
<span class="badge bg-info">${commission.current_students || 0}/${commission.max_students} students</span>
${commission.teacher_name ? `<span class="badge bg-secondary ms-2">${commission.teacher_name}</span>` : ''}
</label>
</div>
</div>
</div>
</div>
`).join('');
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>
`;
}
} 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>
`;
}
}
// 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 %}
+14 -2
View File
@@ -11,13 +11,19 @@
<i class="bi bi-calendar-today text-info"></i> Today's Schedule
<small class="text-muted ms-2">{{ today.strftime('%B %d, %Y') }}</small>
</h1>
<div>
<div>
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-success">
<i class="bi bi-plus-circle"></i> New Reservation
</a>
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-primary ms-2">
<i class="bi bi-list"></i> All Reservations
</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('genetic_algorithm.genetic_optimizer') }}" class="btn btn-info text-white ms-2">
<i class="bi bi-cpu"></i> AI Optimizer
</a>
{% endif %}
</div>
</div>
</div>
@@ -338,13 +344,19 @@
<p class="text-muted">
There are no classroom reservations for {{ today.strftime('%B %d, %Y') }}.
</p>
<div class="mt-3">
<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
</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
</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('genetic_algorithm.genetic_optimizer') }}" class="btn btn-info text-white ms-2">
<i class="bi bi-cpu"></i> AI Room Optimizer
</a>
{% endif %}
</div>
</div>
</div>