feat: implementacion de dashboards por rol, soporte de clases virtuales concurrentes en importador de sheets y cartelera del dia
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user