491 lines
18 KiB
JavaScript
491 lines
18 KiB
JavaScript
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; |