feat: implementacion de dashboards por rol, soporte de clases virtuales concurrentes en importador de sheets y cartelera del dia

This commit is contained in:
2026-09-18 23:04:48 -03:00
parent 47b50ede7e
commit 1080acd43a
240 changed files with 8354 additions and 8886 deletions
+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;