/** * ============================================================================= * SOLVERFORGE QUICKSTART TEMPLATE - APPLICATION JAVASCRIPT * ============================================================================= * * This file contains all the client-side logic for the SolverForge quickstart * template. It implements a "code-link" educational UI that teaches users * how to build the very interface they're looking at. * * FILE STRUCTURE: * --------------- * 1. GLOBAL STATE - Variables tracking UI state, loaded data, solving jobs * 2. INITIALIZATION - Document ready handler and app setup * 3. AJAX CONFIGURATION - jQuery AJAX setup and HTTP method extensions * 4. DEMO DATA LOADING - Fetching and selecting sample datasets * 5. SCHEDULE/SOLUTION LOADING - Getting solution data from backend * 6. RENDERING - Card-based visualization of tasks and resources * 7. KPI UPDATES - Key Performance Indicator card updates * 8. SOLVING OPERATIONS - Start, stop, and poll the solver * 9. SCORE ANALYSIS - Constraint breakdown modal * 10. TAB NAVIGATION HELPERS - Programmatic tab switching * 11. BUILD TAB - Source code viewer with syntax highlighting * 12. INTERACTIVE CODE FEATURES - Click-to-code navigation * 13. NOTIFICATIONS - Toast messages for errors and info * 14. UTILITY FUNCTIONS - Helpers and formatters * 15. RESOURCE & TASK CRUD - Adding and removing entities dynamically * 16. CONSTRAINT WEIGHT CONTROLS - Adjusting optimization weights * * CUSTOMIZATION GUIDE: * -------------------- * When adapting this template for your domain: * * 1. renderSolution() - Change task card layout for your entities * 2. renderResources() - Change resource card layout for your facts * 3. updateKPIs() - Update metrics shown in KPI cards * 4. countViolations() - Implement violation detection for your constraints * * API ENDPOINTS USED: * ------------------- * - GET /demo-data - List available demo datasets * - GET /demo-data/{id} - Get a specific demo dataset * - POST /schedules - Start solving (returns job ID) * - GET /schedules/{jobId} - Get current solution * - DELETE /schedules/{jobId}- Stop solving * - PUT /schedules/analyze - Analyze score breakdown */ // ============================================================================= // 1. GLOBAL STATE // ============================================================================= // These variables track the application state throughout the session. // They are modified by various functions and checked to determine UI behavior. /** * Interval ID for auto-refreshing the solution while solving. * Set by setInterval() when solving starts, cleared when solving stops. * Used to poll the backend for updates every 2 seconds. * * @type {number|null} */ let autoRefreshIntervalId = null; /** * Currently selected demo data ID (e.g., "SMALL", "MEDIUM", "LARGE"). * Set when user selects from the Data dropdown. * Used to fetch the initial dataset before solving. * * @type {string|null} */ let demoDataId = null; /** * Current solving job ID (UUID string from the backend). * Set when solve() successfully starts a job. * Used to poll for updates and stop solving. * * @type {string|null} */ let scheduleId = null; /** * The currently loaded schedule/solution data. * Contains the full problem definition and current solution: * - resources: Array of resource objects (problem facts) * - tasks: Array of task objects (planning entities) * - score: HardSoftScore string (e.g., "0hard/-50soft") * - solverStatus: "NOT_SOLVING" or "SOLVING" * * @type {Object|null} */ let loadedSchedule = null; /** * Currently displayed file in the Build tab code viewer. * Used to track which file is being shown and for copy functionality. * * @type {string} */ let currentFile = 'domain.py'; /** * Cached source code content for the current file. * Populated by loadSourceFile() when fetching from API. * * @type {string} */ let currentFileContent = ''; // ============================================================================= // 2. INITIALIZATION // ============================================================================= // Application startup code. Sets up event handlers and loads initial data. /** * Document ready handler with safe initialization. * * PATTERN: Double-initialization * We use both $(window).on('load') and setTimeout() to ensure initialization * happens even if some external resources load slowly or fail to fire the * load event. * * This pattern is common in SolverForge quickstarts to handle: * - Slow CDN responses * - Browser caching issues * - Race conditions with external scripts */ $(document).ready(function () { let initialized = false; /** * Safe initialization wrapper. * Ensures initializeApp() is only called once. */ function safeInitialize() { if (!initialized) { initialized = true; initializeApp(); } } // Primary: Initialize when all resources (images, scripts) are loaded $(window).on('load', safeInitialize); // Fallback: Initialize after short delay if load event doesn't fire setTimeout(safeInitialize, 100); }); /** * Main initialization function. * * Called once when the page is ready. This function: * 1. Sets up button click handlers * 2. Configures AJAX defaults * 3. Loads the demo data list * 4. Initializes the Build tab code viewer * 5. Sets up code-link click handlers * * CUSTOMIZATION: Add your own initialization code here. */ function initializeApp() { console.log('SolverForge Quickstart Template initializing...'); // ========================================================================= // BUTTON CLICK HANDLERS // ========================================================================= // Solve button - starts the optimization // Connected to solve() function which POSTs to /schedules $("#solveButton").click(function () { solve(); }); // Stop button - terminates solving early // Connected to stopSolving() which DELETEs /schedules/{id} $("#stopSolvingButton").click(function () { stopSolving(); }); // Analyze button - shows score breakdown modal // Connected to analyze() which PUTs to /schedules/analyze $("#analyzeButton").click(function () { analyze(); }); // ========================================================================= // AJAX SETUP & DATA LOADING // ========================================================================= // Configure jQuery AJAX defaults (headers, methods) setupAjax(); // Load the list of available demo datasets fetchDemoData(); // ========================================================================= // BUILD TAB INITIALIZATION // ========================================================================= // Set up file navigator click handlers setupBuildTab(); // Load the default file (domain.py) loadSourceFile('domain.py'); // ========================================================================= // INTERACTIVE CODE FEATURE INITIALIZATION // ========================================================================= // Set up click handlers for code-link elements setupCodeLinkHandlers(); console.log('Initialization complete'); } // ============================================================================= // 3. AJAX CONFIGURATION // ============================================================================= // jQuery AJAX setup for communicating with the backend REST API. /** * Configures jQuery AJAX with proper headers and HTTP method extensions. * * WHAT THIS DOES: * 1. Sets default Content-Type and Accept headers for JSON * 2. Adds $.put() and $.delete() methods to jQuery * (jQuery only has $.get() and $.post() by default) * * WHY WE NEED THIS: * RESTful APIs use all HTTP methods (GET, POST, PUT, DELETE). * The Accept header includes text/plain because job IDs are returned as text. */ function setupAjax() { // Set default headers for all AJAX requests $.ajaxSetup({ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json,text/plain', // text/plain for job ID } }); // Extend jQuery with PUT and DELETE methods // These mirror the signature of $.get() and $.post() jQuery.each(["put", "delete"], function (i, method) { jQuery[method] = function (url, data, callback, type) { // Handle optional parameters (data can be omitted) if (jQuery.isFunction(data)) { type = type || callback; callback = data; data = undefined; } return jQuery.ajax({ url: url, type: method, dataType: type, data: data, success: callback }); }; }); } // ============================================================================= // 4. DEMO DATA LOADING // ============================================================================= // Functions for loading sample datasets from the backend. /** * Fetches the list of available demo datasets and populates the dropdown. * * FLOW: * 1. GET /demo-data returns ["SMALL", "MEDIUM", "LARGE"] (or similar) * 2. For each dataset, create a dropdown menu item * 3. Auto-select and load the first dataset * * CUSTOMIZATION: * The backend demo_data.py defines what datasets are available. * Each dataset is a complete Schedule object with resources and tasks. */ function fetchDemoData() { $.get("/demo-data", function (data) { const dropdown = $("#dataDropdown"); dropdown.empty(); // Create a dropdown item for each available dataset data.forEach(item => { const menuItem = $(`
No tasks in this dataset
'); return; } // Create the task grid container const grid = $(''); // Render each task as a card schedule.tasks.forEach(task => { const card = createTaskCard(task, schedule); grid.append(card); }); panel.append(grid); } /** * Creates a single task card element. * * STRUCTURE: *No resources in this dataset
'); return; } // Render each resource as a card schedule.resources.forEach(resource => { const card = createResourceCard(resource, schedule); panel.append(card); }); } /** * Creates a single resource card element. * * FEATURES: * - Capacity bar showing utilization * - Color-coded: green (<80%), orange (80-100%), red (>100%) * - Skills displayed as tags * * @param {Object} resource - The resource object * @param {Object} schedule - The schedule (for calculating utilization) * @returns {jQuery} The resource card jQuery element */ function createResourceCard(resource, schedule) { // Calculate utilization const totalDuration = schedule.tasks ? schedule.tasks .filter(t => t.resource === resource.name) .reduce((sum, t) => sum + t.duration, 0) : 0; const utilization = resource.capacity > 0 ? (totalDuration / resource.capacity) * 100 : 0; // Determine capacity bar color let fillClass = ''; if (utilization > 100) { fillClass = 'danger'; } else if (utilization > 80) { fillClass = 'warning'; } // Create skills badges HTML const skillsHtml = resource.skills && resource.skills.length > 0 ? resource.skills.map(s => `${s}`).join('') : 'No skills'; // Build the card (escaping name for onclick) const escapedName = resource.name.replace(/'/g, "\\'"); const card = $(`Analyzing...
'); // Check if we have a score to analyze if (!loadedSchedule) { modalContent.html('No data loaded.
'); return; } // Update the score label in the modal header $('#scoreAnalysisScore').text(loadedSchedule.score || '?'); // Fetch the score analysis from the backend $.put("/schedules/analyze", JSON.stringify(loadedSchedule), function (scoreAnalysis) { renderScoreAnalysis(scoreAnalysis, modalContent); }).fail(function (xhr, ajaxOptions, thrownError) { modalContent.html('Failed to analyze score.
'); console.error('Failed to analyze score:', xhr.responseText); }, "json"); } /** * Renders the score analysis table in the modal. * * TABLE COLUMNS: * - Icon: Warning/check status * - Constraint: Name of the constraint * - Type: hard/soft * - Matches: Number of violations * - Weight: Constraint weight * - Score: Score contribution * * @param {Object} scoreAnalysis - The analysis data from the backend * @param {jQuery} container - The container element to render into */ function renderScoreAnalysis(scoreAnalysis, container) { container.empty(); let constraints = scoreAnalysis.constraints || []; if (constraints.length === 0) { container.html('No constraint data available.
'); return; } // Sort constraints: violated hard constraints first, then by impact constraints.sort((a, b) => { let aComponents = getScoreComponents(a.score); let bComponents = getScoreComponents(b.score); // Hard constraints with negative score first if (aComponents.hard < 0 && bComponents.hard >= 0) return -1; if (aComponents.hard >= 0 && bComponents.hard < 0) return 1; // Then by absolute hard score if (Math.abs(aComponents.hard) !== Math.abs(bComponents.hard)) { return Math.abs(bComponents.hard) - Math.abs(aComponents.hard); } // Then by soft score return Math.abs(bComponents.soft) - Math.abs(aComponents.soft); }); // Build the analysis table let html = '| Constraint | Type | Matches | Score | |
|---|---|---|---|---|
| ${icon} | ${constraint.name} | ${typeBadge} | ${matchCount} | ${scoreDisplay} |
element
codeEl.text(currentFileContent);
codeEl.attr('class', `language-${language}`);
// Trigger Prism.js syntax highlighting
// This transforms plain text into highlighted HTML with line numbers
if (typeof Prism !== 'undefined') {
Prism.highlightElement(codeEl[0]);
}
// RUNTIME LINE DETECTION: If a section was requested, find and scroll to it
// We do this AFTER Prism highlighting because:
// 1. The content needs to be rendered before we can scroll
// 2. Prism adds line-numbers-rows elements we use for accurate scrolling
if (section) {
// Small delay to ensure Prism.js has finished rendering line numbers
// Prism's highlightElement is synchronous, but DOM updates need a tick
setTimeout(() => {
const lineNumber = findSectionLineNumber(currentFileContent, section, language);
if (lineNumber > 0) {
console.log(`Found "${section}" at line ${lineNumber}`);
scrollToLine(lineNumber);
} else {
console.warn(`Section "${section}" not found in ${filename}`);
}
}, 50);
}
}).fail(function(xhr, status, error) {
console.error('Failed to load source file:', error);
codeEl.text('// Error loading file: ' + error);
currentFileContent = '';
});
}
/**
* Finds the line number where a section (function/class) is defined.
*
* RUNTIME LINE DETECTION EXPLAINED:
* ---------------------------------
* This is the core of our "smart scroll" feature. Instead of hardcoding line
* numbers (which break when code changes), we search the actual file content
* at runtime to find where a function or class is defined.
*
* HOW IT WORKS:
* 1. Split the file content into lines
* 2. Build regex patterns based on the language and section name
* 3. Search each line for a match
* 4. Return the 1-based line number (or 0 if not found)
*
* PATTERNS SEARCHED:
* - Python: "def section_name(" or "class SectionName"
* - JavaScript: "function sectionName(" or "sectionName = function"
* or "const/let/var sectionName" or "sectionName(" at definition
*
* WHY THIS APPROACH:
* - Resilient: Works even as code is edited and line numbers change
* - Flexible: Can find functions, classes, or any named definition
* - Language-aware: Uses appropriate patterns for Python vs JavaScript
*
* TRADE-OFFS:
* - May not find minified code or unusual formatting
* - Could match wrong occurrence if same name appears multiple times
* (we return the FIRST match, which is usually the definition)
*
* @param {string} content - The full file content
* @param {string} sectionName - The function/class name to find
* @param {string} language - The file language ('python', 'javascript', 'markup')
* @returns {number} 1-based line number, or 0 if not found
*/
function findSectionLineNumber(content, sectionName, language) {
if (!content || !sectionName) {
return 0;
}
// Split content into lines for line-by-line search
const lines = content.split('\n');
// Build search patterns based on language
// We use multiple patterns to catch different definition styles
const patterns = [];
if (language === 'python') {
// Python patterns:
// - "def function_name(" - function definition
// - "class ClassName" - class definition (may or may not have parens)
// - "@decorator" followed by def - decorated functions
patterns.push(new RegExp(`^\\s*def\\s+${sectionName}\\s*\\(`));
patterns.push(new RegExp(`^\\s*class\\s+${sectionName}\\b`));
patterns.push(new RegExp(`^\\s*async\\s+def\\s+${sectionName}\\s*\\(`));
} else if (language === 'javascript') {
// JavaScript patterns:
// - "function functionName(" - classic function declaration
// - "functionName = function" - function expression
// - "const/let/var functionName" - modern declaration
// - "functionName(" in object/class context
// - "async function" variants
patterns.push(new RegExp(`^\\s*function\\s+${sectionName}\\s*\\(`));
patterns.push(new RegExp(`^\\s*async\\s+function\\s+${sectionName}\\s*\\(`));
patterns.push(new RegExp(`^\\s*(const|let|var)\\s+${sectionName}\\s*=`));
patterns.push(new RegExp(`^\\s*${sectionName}\\s*[:=]\\s*(async\\s+)?function`));
patterns.push(new RegExp(`^\\s*${sectionName}\\s*\\(`)); // Method shorthand
} else {
// HTML/markup: search for id or class attributes
patterns.push(new RegExp(`id=["']${sectionName}["']`));
patterns.push(new RegExp(`class=["'][^"']*${sectionName}[^"']*["']`));
}
// Search each line for any of our patterns
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
for (const pattern of patterns) {
if (pattern.test(line)) {
// Return 1-based line number (lines array is 0-indexed)
return i + 1;
}
}
}
// Fallback: simple substring search for the section name
// This catches cases our patterns missed (e.g., comments mentioning the section)
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes(sectionName)) {
console.log(`Fallback match for "${sectionName}" at line ${i + 1}`);
return i + 1;
}
}
return 0; // Not found
}
/**
* Scrolls the code viewer to a specific line.
*
* Uses multiple approaches to accurately scroll to a line:
* 1. Try to find Prism.js line-numbers-rows spans
* 2. Fall back to measuring line height from rendered code
*
* @param {number} lineNumber - The line to scroll to
*/
function scrollToLine(lineNumber) {
const viewer = $('.code-viewer-body');
const preEl = viewer.find('pre')[0];
const codeEl = viewer.find('code')[0];
if (!preEl || !codeEl) {
console.warn('No code element found for scrolling');
return;
}
let offset = 0;
// Method 1: Try to use Prism.js line-numbers-rows spans
const lineNumbersRows = preEl.querySelector('.line-numbers-rows');
if (lineNumbersRows && lineNumbersRows.children.length > 0) {
// Get the height of a single line number span
const firstLineSpan = lineNumbersRows.children[0];
if (firstLineSpan) {
const lineHeight = firstLineSpan.getBoundingClientRect().height;
offset = (lineNumber - 1) * lineHeight;
console.log(`Using line-numbers-rows method: lineHeight=${lineHeight}px, offset=${offset}px`);
}
}
// Method 2: Fall back to measuring from pre element
if (offset === 0) {
const preStyle = window.getComputedStyle(preEl);
let lineHeight = parseFloat(preStyle.lineHeight);
// If lineHeight is 'normal', compute from font size
if (isNaN(lineHeight) || lineHeight <= 0) {
const fontSize = parseFloat(preStyle.fontSize) || 14;
lineHeight = fontSize * 1.5;
}
offset = (lineNumber - 1) * lineHeight;
console.log(`Using fallback method: lineHeight=${lineHeight}px, offset=${offset}px`);
}
// Add padding offset from the pre element
const preStyle = window.getComputedStyle(preEl);
const paddingTop = parseFloat(preStyle.paddingTop) || 0;
offset += paddingTop;
// Animate scroll with some padding above the target line
const viewerHeight = viewer.height();
const scrollTarget = Math.max(0, offset - (viewerHeight * 0.2));
viewer.animate({
scrollTop: scrollTarget
}, 300);
console.log(`Scrolling to line ${lineNumber}, final offset ${scrollTarget}px`);
// Highlight the line briefly
highlightLine(lineNumber);
}
/**
* Briefly highlights a line in the code viewer.
*
* @param {number} lineNumber - The line to highlight
*/
function highlightLine(lineNumber) {
// Remove any existing highlights
$('.line-highlight').remove();
const viewer = $('.code-viewer-body');
const preEl = viewer.find('pre')[0];
if (!preEl) return;
// Get line height
const lineNumbersRows = preEl.querySelector('.line-numbers-rows');
let lineHeight = 21; // default
if (lineNumbersRows && lineNumbersRows.children.length > 0) {
lineHeight = lineNumbersRows.children[0].getBoundingClientRect().height;
}
const preStyle = window.getComputedStyle(preEl);
const paddingTop = parseFloat(preStyle.paddingTop) || 0;
// Create highlight element
const highlight = $('');
highlight.css({
position: 'absolute',
left: 0,
right: 0,
top: paddingTop + (lineNumber - 1) * lineHeight,
height: lineHeight,
background: 'rgba(62, 0, 255, 0.15)',
borderLeft: '3px solid #3E00FF',
pointerEvents: 'none',
zIndex: 10
});
// Add to pre element (needs relative positioning)
$(preEl).css('position', 'relative').append(highlight);
// Fade out after 2 seconds
setTimeout(() => {
highlight.fadeOut(500, function() {
$(this).remove();
});
}, 2000);
}
/**
* Copies the current code to clipboard.
*/
function copyCurrentCode() {
const code = currentFileContent || '';
if (!code) {
showNotification('No code loaded to copy', 'warning');
return;
}
navigator.clipboard.writeText(code).then(() => {
showNotification('Code copied to clipboard!', 'success');
}).catch(err => {
console.error('Failed to copy:', err);
showNotification('Failed to copy code', 'danger');
});
}
// =============================================================================
// 12. INTERACTIVE CODE FEATURES - Click-to-Code Navigation
// =============================================================================
// The "code-link" feature: clicking UI elements reveals their source code.
/**
* Sets up click handlers for code-link elements.
*
* Elements with class="code-link" and data-target="file:section"
* will navigate to the Build tab and highlight the relevant code.
*
* EXAMPLES:
* - data-target="app.js:updateKPIs" -> app.js, scrolls to updateKPIs function
* - data-target="constraints.py:required_skill" -> constraints.py
*/
function setupCodeLinkHandlers() {
// Main code-link elements
$(document).on('click', '.code-link', function(e) {
// Don't trigger for nested clickable elements
if ($(e.target).closest('.btn').length > 0) {
return; // Let button clicks work normally
}
const target = $(this).data('target');
if (target) {
navigateToCode(target);
}
});
// Constraint badges
$(document).on('click', '.constraint-badge', function(e) {
e.stopPropagation();
const target = $(this).data('target');
if (target) {
navigateToCode(target);
}
});
}
/**
* Navigates to a specific code location from an code-link target.
*
* TARGET FORMAT: "filename:section"
* - filename: The file to show (e.g., "app.js", "constraints.py")
* - section: Optional section/function name to scroll to (e.g., "updateKPIs")
*
* RUNTIME LINE DETECTION:
* Unlike static line numbers that would break when code changes, this function
* uses runtime search to find the section. After the file loads from the API,
* we search the actual content for the section name (function/class definition)
* and scroll to where it's found. This keeps code-links working even as code evolves.
*
* @param {string} target - The target in "file:section" format
*/
function navigateToCode(target) {
console.log('Navigating to code:', target);
// Parse target: "filename:section" -> ["filename", "section"]
// The section is optional - if not provided, we just show the file from the top
const [filename, section] = target.split(':');
// Switch to Build tab using Bootstrap 5 Tab API
// We access the nav-link element and create a Tab instance to show it
const tabEl = document.querySelector('[data-bs-target="#build"]');
if (tabEl) {
const tab = new bootstrap.Tab(tabEl);
tab.show();
}
// Load the file after a short delay to ensure tab transition completes
// The delay is needed because Bootstrap's tab.show() is asynchronous
setTimeout(() => {
// Update file navigator active state for visual feedback
$('.file-item').removeClass('active');
$(`.file-item[data-file="${filename}"]`).addClass('active');
// Load the file, passing the section name for line scrolling
// If section is provided, loadSourceFile will search for it after loading
loadSourceFile(filename, section);
}, 100);
// Show notification with file and section info
const sectionInfo = section ? ` → ${section}` : '';
showNotification(`Viewing ${filename}${sectionInfo}`, 'info');
}
// =============================================================================
// 13. NOTIFICATIONS
// =============================================================================
// Toast-style notification messages.
/**
* Shows a notification toast message.
*
* TYPES:
* - 'success': Green checkmark
* - 'danger': Red X
* - 'warning': Yellow warning
* - 'info': Blue info
*
* @param {string} message - The message to display
* @param {string} type - The notification type (success, danger, warning, info)
*/
function showNotification(message, type = 'info') {
const panel = $('#notificationPanel');
// Create the toast
const toast = $(`
${message}
`);
panel.append(toast);
// Auto-dismiss after 5 seconds
setTimeout(() => {
toast.alert('close');
}, 5000);
}
// =============================================================================
// 14. UTILITY FUNCTIONS
// =============================================================================
// Helper functions used throughout the application.
/**
* Escapes HTML special characters to prevent XSS.
*
* @param {string} text - The text to escape
* @returns {string} Escaped text safe for HTML insertion
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// =============================================================================
// 15. RESOURCE & TASK CRUD OPERATIONS
// =============================================================================
// Functions for adding and removing resources and tasks dynamically.
/**
* Shows the Add Resource modal dialog.
*
* @param {Event} event - Click event (to stop propagation)
*/
function showAddResourceModal(event) {
event.stopPropagation();
// Clear previous values
$('#resourceName').val('');
$('#resourceCapacity').val(100);
$('#resourceSkills').val('');
// Show modal
const modal = new bootstrap.Modal('#addResourceModal');
modal.show();
}
/**
* Adds a new resource to the schedule.
*
* Reads values from the Add Resource modal form and adds
* a new resource to loadedSchedule.resources.
*/
function addResource() {
const name = $('#resourceName').val().trim();
const capacity = parseInt($('#resourceCapacity').val()) || 100;
const skillsStr = $('#resourceSkills').val().trim();
const skills = skillsStr ? skillsStr.split(',').map(s => s.trim().toLowerCase()) : [];
// Validate
if (!name) {
showNotification('Please enter a resource name', 'warning');
return;
}
// Check for duplicate
if (loadedSchedule && loadedSchedule.resources) {
if (loadedSchedule.resources.some(r => r.name === name)) {
showNotification('A resource with this name already exists', 'warning');
return;
}
}
// Initialize schedule if needed
if (!loadedSchedule) {
loadedSchedule = { resources: [], tasks: [] };
}
if (!loadedSchedule.resources) {
loadedSchedule.resources = [];
}
// Add the resource
loadedSchedule.resources.push({
name: name,
capacity: capacity,
skills: skills
});
// Close modal and re-render
bootstrap.Modal.getInstance('#addResourceModal').hide();
renderSchedule(loadedSchedule);
showNotification(`Added resource: ${name}`, 'success');
}
/**
* Removes a resource from the schedule.
*
* @param {string} resourceName - Name of the resource to remove
* @param {Event} event - Click event (to stop propagation)
*/
function removeResource(resourceName, event) {
event.stopPropagation();
if (!loadedSchedule || !loadedSchedule.resources) {
return;
}
// Remove the resource
loadedSchedule.resources = loadedSchedule.resources.filter(r => r.name !== resourceName);
// Unassign any tasks assigned to this resource
if (loadedSchedule.tasks) {
loadedSchedule.tasks.forEach(task => {
if (task.resource === resourceName) {
task.resource = null;
}
});
}
// Re-render
renderSchedule(loadedSchedule);
showNotification(`Removed resource: ${resourceName}`, 'info');
}
/**
* Shows the Add Task modal dialog.
*
* @param {Event} event - Click event (to stop propagation)
*/
function showAddTaskModal(event) {
event.stopPropagation();
// Clear previous values
$('#taskName').val('');
$('#taskDuration').val(30);
$('#taskSkill').val('');
// Show modal
const modal = new bootstrap.Modal('#addTaskModal');
modal.show();
}
/**
* Adds a new task to the schedule.
*
* Reads values from the Add Task modal form and adds
* a new task to loadedSchedule.tasks.
*/
function addTask() {
const name = $('#taskName').val().trim();
const duration = parseInt($('#taskDuration').val()) || 30;
const requiredSkill = $('#taskSkill').val().trim().toLowerCase();
// Validate
if (!name) {
showNotification('Please enter a task name', 'warning');
return;
}
// Initialize schedule if needed
if (!loadedSchedule) {
loadedSchedule = { resources: [], tasks: [] };
}
if (!loadedSchedule.tasks) {
loadedSchedule.tasks = [];
}
// Generate unique ID
const existingIds = loadedSchedule.tasks.map(t => t.id);
let newId = `task-${loadedSchedule.tasks.length + 1}`;
let counter = loadedSchedule.tasks.length + 1;
while (existingIds.includes(newId)) {
counter++;
newId = `task-${counter}`;
}
// Add the task
loadedSchedule.tasks.push({
id: newId,
name: name,
duration: duration,
requiredSkill: requiredSkill || '',
resource: null
});
// Close modal and re-render
bootstrap.Modal.getInstance('#addTaskModal').hide();
renderSchedule(loadedSchedule);
showNotification(`Added task: ${name}`, 'success');
}
/**
* Removes a task from the schedule.
*
* @param {string} taskId - ID of the task to remove
* @param {Event} event - Click event (to stop propagation)
*/
function removeTask(taskId, event) {
event.stopPropagation();
if (!loadedSchedule || !loadedSchedule.tasks) {
return;
}
// Find task name for notification
const task = loadedSchedule.tasks.find(t => t.id === taskId);
const taskName = task ? task.name : taskId;
// Remove the task
loadedSchedule.tasks = loadedSchedule.tasks.filter(t => t.id !== taskId);
// Re-render
renderSchedule(loadedSchedule);
showNotification(`Removed task: ${taskName}`, 'info');
}
// =============================================================================
// 16. CONSTRAINT WEIGHT CONTROLS
// =============================================================================
// Functions for adjusting constraint weights via sliders.
/**
* Default constraint weight values.
* Hard constraints default to 100, soft constraints to 50.
*/
const DEFAULT_WEIGHTS = {
RequiredSkill: 100,
ResourceCapacity: 100,
MinimizeDuration: 50,
BalanceLoad: 50
};
/**
* Updates the displayed value for a constraint weight slider.
*
* Called by oninput on each slider.
*
* @param {string} constraintName - Name of the constraint (e.g., "RequiredSkill")
*/
function updateWeightDisplay(constraintName) {
const value = $(`#weight${constraintName}`).val();
$(`#weight${constraintName}Value`).text(value);
}
/**
* Resets all constraint weights to their default values.
*/
function resetConstraintWeights() {
Object.keys(DEFAULT_WEIGHTS).forEach(name => {
$(`#weight${name}`).val(DEFAULT_WEIGHTS[name]);
$(`#weight${name}Value`).text(DEFAULT_WEIGHTS[name]);
});
showNotification('Constraint weights reset to defaults', 'info');
}
/**
* Gets the current constraint weights from the sliders.
*
* @returns {Object} Object with constraint names and their weights (0-100)
*/
function getConstraintWeights() {
return {
required_skill: parseInt($('#weightRequiredSkill').val()) || 100,
resource_capacity: parseInt($('#weightResourceCapacity').val()) || 100,
minimize_duration: parseInt($('#weightMinimizeDuration').val()) || 50,
balance_load: parseInt($('#weightBalanceLoad').val()) || 50
};
}