let autoRefreshIntervalId = null; const formatter = JSJoda.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); const startTime = formatter.format(JSJoda.LocalDateTime.now().withHour(20).withMinute(0).withSecond(0)); const endTime = formatter.format(JSJoda.LocalDateTime.now().plusDays(1).withHour(8).withMinute(0).withSecond(0)); const zoomMin = 1000 * 60 * 60 // one hour in milliseconds const zoomMax = 4 * 1000 * 60 * 60 * 24 // 5 days in milliseconds const byTimelineOptions = { timeAxis: {scale: "hour", step: 1}, orientation: {axis: "top"}, stack: false, xss: {disabled: true}, // Items are XSS safe through JQuery zoomMin: zoomMin, zoomMax: zoomMax, showCurrentTime: false, hiddenDates: [ { start: startTime, end: endTime, repeat: 'daily' } ], }; const byRoomPanel = document.getElementById("byRoomPanel"); let byRoomGroupData = new vis.DataSet(); let byRoomItemData = new vis.DataSet(); let byRoomTimeline = new vis.Timeline(byRoomPanel, byRoomItemData, byRoomGroupData, byTimelineOptions); const byPersonPanel = document.getElementById("byPersonPanel"); let byPersonGroupData = new vis.DataSet(); let byPersonItemData = new vis.DataSet(); let byPersonTimeline = new vis.Timeline(byPersonPanel, byPersonItemData, byPersonGroupData, byTimelineOptions); let scheduleId = null; let loadedSchedule = null; let viewType = "R"; let selectedDemoData = "MEDIUM"; // Default demo data size let analyzeCache = null; // Cache for solver's constraint analysis (assignmentId -> violations) let appInitialized = false; $(document).ready(function () { // Ensure all resources are loaded before initializing $(window).on('load', function() { if (!appInitialized) { appInitialized = true; initializeApp(); } }); // Fallback if window load event doesn't fire setTimeout(function() { if (!appInitialized) { appInitialized = true; initializeApp(); } }, 100); }); function initializeApp() { replaceQuickstartSolverForgeAutoHeaderFooter(); $("#solveButton").click(function () { solve(); }); $("#stopSolvingButton").click(function () { stopSolving(); }); $("#analyzeButton").click(function () { analyze(); }); $("#byRoomTab").click(function () { viewType = "R"; byRoomTimeline.redraw(); refreshSchedule(); }); $("#byPersonTab").click(function () { viewType = "P"; byPersonTimeline.redraw(); refreshSchedule(); }); setupAjax(); loadDemoDataDropdown(); refreshSchedule(); } function loadDemoDataDropdown() { $.getJSON("/demo-data", function (demoDataList) { const dropdown = $("#testDataButton"); dropdown.empty(); demoDataList.forEach(function (name) { const isSelected = name === selectedDemoData; const item = $(``) .text(name) .css("font-weight", isSelected ? "bold" : "normal") .click(function (e) { e.preventDefault(); selectDemoData(name); }); dropdown.append(item); }); }); } function selectDemoData(name) { selectedDemoData = name; scheduleId = null; // Reset solver job loadDemoDataDropdown(); // Refresh dropdown to show selection refreshSchedule(); } function setupAjax() { $.ajaxSetup({ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json,text/plain', // plain text is required by solve() returning UUID of the solver job } }); // Extend jQuery to support $.put() and $.delete() jQuery.each(["put", "delete"], function (i, method) { jQuery[method] = function (url, data, callback, type) { if (jQuery.isFunction(data)) { type = type || callback; callback = data; data = undefined; } return jQuery.ajax({ url: url, type: method, dataType: type, data: data, success: callback }); }; }); } function refreshSchedule() { let path; if (scheduleId === null) { path = "/demo-data/" + selectedDemoData; } else { path = "/schedules/" + scheduleId; } $.getJSON(path, function (schedule) { loadedSchedule = schedule; $('#exportData').attr('href', 'data:text/plain;charset=utf-8,' + JSON.stringify(loadedSchedule)); renderSchedule(schedule); }) .fail(function (xhr, ajaxOptions, thrownError) { showError("Getting the schedule has failed.", xhr); refreshSolvingButtons(false); }); } function renderSchedule(schedule) { refreshSolvingButtons(schedule.solverStatus != null && schedule.solverStatus !== "NOT_SOLVING"); $("#score").text("Score: " + (schedule.score == null ? "?" : schedule.score)); // Fetch constraint analysis from solver if we have a score if (schedule.score) { fetchConstraintAnalysis(schedule); } else { // No score yet - clear cache and render with no violations analyzeCache = null; renderViews(schedule); } } function renderViews(schedule) { if (viewType === "R") { renderScheduleByRoom(schedule); } if (viewType === "P") { renderScheduleByPerson(schedule); } } function fetchConstraintAnalysis(schedule) { $.ajax({ url: "/schedules/analyze", type: "PUT", data: JSON.stringify(schedule), contentType: "application/json", success: function(response) { // Build mapping: assignmentId -> { hard: [], medium: [], soft: [] } analyzeCache = new Map(); for (const constraint of response.constraints) { const type = getConstraintType(constraint.weight); for (const match of constraint.matches) { const assignmentIds = extractAssignmentIds(match.justification, loadedSchedule); for (const id of assignmentIds) { if (!analyzeCache.has(id)) { analyzeCache.set(id, { hard: [], medium: [], soft: [] }); } analyzeCache.get(id)[type].push({ constraint: constraint.name, score: match.score }); } } } // Re-render with new analysis data renderViews(loadedSchedule); }, error: function(xhr, status, error) { console.warn("Failed to fetch constraint analysis:", error); analyzeCache = null; renderViews(schedule); } }); } function getConstraintType(weight) { // Weight format: "0hard/0medium/-1soft" or "1hard/0medium/0soft" // Extract the non-zero component const hardMatch = weight.match(/(-?\d+)hard/); const mediumMatch = weight.match(/(-?\d+)medium/); const softMatch = weight.match(/(-?\d+)soft/); if (hardMatch && parseInt(hardMatch[1], 10) !== 0) return 'hard'; if (mediumMatch && parseInt(mediumMatch[1], 10) !== 0) return 'medium'; if (softMatch && parseInt(softMatch[1], 10) !== 0) return 'soft'; return 'soft'; // Default } function extractAssignmentIds(justification, schedule) { const ids = new Set(); if (!justification?.facts) return [...ids]; // Build meeting-to-assignment lookup const meetingToAssignment = new Map(); if (schedule?.meetingAssignments) { for (const a of schedule.meetingAssignments) { const meetingId = typeof a.meeting === 'object' ? a.meeting.id : a.meeting; meetingToAssignment.set(meetingId, a.id); } } for (const fact of justification.facts) { if (fact.type === 'assignment' && fact.id) { ids.add(fact.id); } else if (fact.type === 'attendance' && fact.meetingId) { const assignmentId = meetingToAssignment.get(fact.meetingId); if (assignmentId) ids.add(assignmentId); } } return [...ids]; } function getConflictStatus(assignmentId) { // Use solver's constraint analysis if available (per-assignment violations) if (analyzeCache && analyzeCache.has(assignmentId)) { const violations = analyzeCache.get(assignmentId); if (violations.hard.length > 0) { return { status: 'hard', icon: '', style: 'background-color: #fee2e2; border-left: 4px solid #dc3545;', reason: violations.hard.map(v => v.constraint).join(', ') }; } if (violations.medium.length > 0) { return { status: 'medium', icon: '', style: 'background-color: #fef3c7; border-left: 4px solid #ffc107;', reason: violations.medium.map(v => v.constraint).join(', ') }; } // Don't show soft violations in timeline - they're optimization trade-offs // (e.g., "Overlapping meetings" fires for ANY parallel meetings, even in different rooms) // Users can see soft violations in the "Analyze" modal instead } // No hard/medium violations - show green (feasible solution) return { status: 'ok', icon: '', style: 'background-color: #d1fae5; border-left: 4px solid #10b981;', reason: '' }; } function calculatePersonWorkload(schedule) { const personMeetingCount = new Map(); if (!schedule.meetingAssignments || !schedule.meetings) { return personMeetingCount; } const meetingMap = new Map(); schedule.meetings.forEach(m => meetingMap.set(m.id, m)); // Count meetings per person (assigned meetings only) schedule.meetingAssignments.forEach(assignment => { if (assignment.room == null || assignment.startingTimeGrain == null) return; const meeting = typeof assignment.meeting === 'string' ? meetingMap.get(assignment.meeting) : assignment.meeting; if (!meeting) return; // Count required attendees (meeting.requiredAttendances || []).forEach(att => { const personId = att.person?.id || att.person; personMeetingCount.set(personId, (personMeetingCount.get(personId) || 0) + 1); }); // Count preferred attendees (meeting.preferredAttendances || []).forEach(att => { const personId = att.person?.id || att.person; personMeetingCount.set(personId, (personMeetingCount.get(personId) || 0) + 1); }); }); return personMeetingCount; } function getWorkloadBadge(meetingCount) { if (meetingCount === 0) { return '0'; } else if (meetingCount <= 5) { return `${meetingCount}`; } else if (meetingCount <= 9) { return `${meetingCount}`; } else { return `${meetingCount}`; } } function analyzeUnassignedReason(meeting, schedule) { const reasons = []; const totalAttendees = (meeting.requiredAttendances?.length || 0) + (meeting.preferredAttendances?.length || 0); // Check room capacity const largestRoomCapacity = Math.max(...schedule.rooms.map(r => r.capacity)); if (totalAttendees > largestRoomCapacity) { reasons.push(`Needs ${totalAttendees} capacity, largest room has ${largestRoomCapacity}`); } // Check if meeting duration is very long const durationHours = ((meeting.durationInGrains ?? meeting.duration_in_grains) * 15) / 60; if (durationHours > 3) { reasons.push(`Long meeting (${durationHours}h) - fewer available slots`); } // Check if required attendees are heavily booked const meetingMap = new Map(); schedule.meetings.forEach(m => meetingMap.set(m.id, m)); const requiredAttendeeIds = new Set((meeting.requiredAttendances || []).map(a => a.person?.id || a.person)); let busyAttendeesCount = 0; schedule.meetingAssignments.forEach(assignment => { if (assignment.room == null || assignment.startingTimeGrain == null) return; if (assignment.meeting === meeting.id) return; const otherMeeting = typeof assignment.meeting === 'string' ? meetingMap.get(assignment.meeting) : assignment.meeting; if (!otherMeeting) return; const otherRequiredIds = new Set((otherMeeting.requiredAttendances || []).map(a => a.person?.id || a.person)); for (const id of requiredAttendeeIds) { if (otherRequiredIds.has(id)) { busyAttendeesCount++; break; } } }); if (busyAttendeesCount > 0 && requiredAttendeeIds.size > 0) { const percentBusy = Math.round((busyAttendeesCount / schedule.meetingAssignments.filter(a => a.room && a.startingTimeGrain).length) * 100); if (percentBusy > 30) { reasons.push(`Required attendees have many existing meetings`); } } // If still solving, add generic reason if (reasons.length === 0) { reasons.push(`Being optimized by solver`); } return reasons; } function renderScheduleByRoom(schedule) { const unassigned = $("#unassigned"); unassigned.children().remove(); byRoomGroupData.clear(); byRoomItemData.clear(); // Check if schedule.rooms exists and is an array if (!schedule.rooms || !Array.isArray(schedule.rooms)) { console.warn('schedule.rooms is not available or not an array:', schedule.rooms); return; } $.each(schedule.rooms.sort((e1, e2) => e1.name.localeCompare(e2.name)), (_, room) => { let content = `
${room.name}
`; byRoomGroupData.add({ id: room.id, content: content, }); }); const meetingMap = new Map(); if (schedule.meetings && Array.isArray(schedule.meetings)) { schedule.meetings.forEach(m => meetingMap.set(m.id, m)); } const timeGrainMap = new Map(); if (schedule.timeGrains && Array.isArray(schedule.timeGrains)) { schedule.timeGrains.forEach(t => timeGrainMap.set(t.id, t)); } const roomMap = new Map(); if (schedule.rooms && Array.isArray(schedule.rooms)) { schedule.rooms.forEach(r => roomMap.set(r.id, r)); } if (!schedule.meetingAssignments || !Array.isArray(schedule.meetingAssignments)) { console.warn('schedule.meetingAssignments is not available or not an array:', schedule.meetingAssignments); return; } $.each(schedule.meetingAssignments, (_, assignment) => { // Handle both string ID and full object for meeting reference const meet = typeof assignment.meeting === 'string' ? meetingMap.get(assignment.meeting) : assignment.meeting; // Handle both string ID and full object for room reference const room = typeof assignment.room === 'string' ? roomMap.get(assignment.room) : assignment.room; // Handle both string ID and full object for timeGrain reference const timeGrain = typeof assignment.startingTimeGrain === 'string' ? timeGrainMap.get(assignment.startingTimeGrain) : assignment.startingTimeGrain; // Skip if meeting is not found if (!meet) { console.warn(`Meeting not found for assignment ${assignment.id}`); return; } if (room == null || timeGrain == null) { const durationHours = ((meet.durationInGrains ?? meet.duration_in_grains) * 15) / 60; const requiredCount = meet.requiredAttendances?.length || 0; const preferredCount = meet.preferredAttendances?.length || 0; const totalAttendees = requiredCount + preferredCount; // Analyze why unassigned const reasons = analyzeUnassignedReason(meet, schedule); const unassignedElement = $(`
`) .append($(`
`).text(meet.topic)) .append($(`

`).html(`${durationHours} hour(s)`)) .append($(`

`).html(`${totalAttendees} attendees (${requiredCount} required, ${preferredCount} preferred)`)); if (reasons.length > 0) { const reasonsList = $(`

`); reasonsList.append($(`Possible issues:`)); reasons.forEach(reason => { reasonsList.append($(`
`).html(`${reason}`)); }); unassignedElement.append(reasonsList); } unassigned.append($(`
`).append($(`
`).append(unassignedElement))); } else { const conflictStatus = getConflictStatus(assignment.id); const byRoomElement = $("
") .append($("
") .append($(conflictStatus.icon)) .append($(`
`).text(meet.topic))); const startDate = JSJoda.LocalDate.now().withDayOfYear(timeGrain.dayOfYear ?? timeGrain.day_of_year); const startTime = JSJoda.LocalTime.of(0, 0, 0, 0) .plusMinutes((timeGrain.startingMinuteOfDay ?? timeGrain.starting_minute_of_day)); const startDateTime = JSJoda.LocalDateTime.of(startDate, startTime); const endDateTime = startTime.plusMinutes((meet.durationInGrains ?? meet.duration_in_grains) * 15); byRoomItemData.add({ id: assignment.id, group: typeof room === 'string' ? room : room.id, content: byRoomElement.html(), start: startDateTime.toString(), end: endDateTime.toString(), style: `min-height: 50px; ${conflictStatus.style}`, title: conflictStatus.reason || undefined }); } }); byRoomTimeline.setWindow(JSJoda.LocalDateTime.now().plusDays(1).withHour(8).toString(), JSJoda.LocalDateTime.now().plusDays(1).withHour(17).withMinute(45).toString()); } function renderScheduleByPerson(schedule) { const unassigned = $("#unassigned"); unassigned.children().remove(); byPersonGroupData.clear(); byPersonItemData.clear(); // Check if schedule.people exists and is an array if (!schedule.people || !Array.isArray(schedule.people)) { console.warn('schedule.people is not available or not an array:', schedule.people); return; } // Calculate meeting count per person for workload indicators const personMeetingCount = calculatePersonWorkload(schedule); $.each(schedule.people.sort((e1, e2) => e1.fullName.localeCompare(e2.fullName)), (_, person) => { const meetingCount = personMeetingCount.get(person.id) || 0; const workloadBadge = getWorkloadBadge(meetingCount); let content = `
${person.fullName}
${workloadBadge}
`; byPersonGroupData.add({ id: person.id, content: content, }); }); const meetingMap = new Map(); if (schedule.meetings && Array.isArray(schedule.meetings)) { schedule.meetings.forEach(m => meetingMap.set(m.id, m)); } const timeGrainMap = new Map(); if (schedule.timeGrains && Array.isArray(schedule.timeGrains)) { schedule.timeGrains.forEach(t => timeGrainMap.set(t.id, t)); } const roomMap = new Map(); if (schedule.rooms && Array.isArray(schedule.rooms)) { schedule.rooms.forEach(r => roomMap.set(r.id, r)); } if (!schedule.meetingAssignments || !Array.isArray(schedule.meetingAssignments)) { console.warn('schedule.meetingAssignments is not available or not an array:', schedule.meetingAssignments); return; } $.each(schedule.meetingAssignments, (_, assignment) => { // Handle both string ID and full object for meeting reference const meet = typeof assignment.meeting === 'string' ? meetingMap.get(assignment.meeting) : assignment.meeting; // Handle both string ID and full object for room reference const room = typeof assignment.room === 'string' ? roomMap.get(assignment.room) : assignment.room; // Handle both string ID and full object for timeGrain reference const timeGrain = typeof assignment.startingTimeGrain === 'string' ? timeGrainMap.get(assignment.startingTimeGrain) : assignment.startingTimeGrain; // Skip if meeting is not found if (!meet) { console.warn(`Meeting not found for assignment ${assignment.id}`); return; } if (room == null || timeGrain == null) { const durationHours = ((meet.durationInGrains ?? meet.duration_in_grains) * 15) / 60; const requiredCount = meet.requiredAttendances?.length || 0; const preferredCount = meet.preferredAttendances?.length || 0; const totalAttendees = requiredCount + preferredCount; // Analyze why unassigned const reasons = analyzeUnassignedReason(meet, schedule); const unassignedElement = $(`
`) .append($(`
`).text(meet.topic)) .append($(`

`).html(`${durationHours} hour(s)`)) .append($(`

`).html(`${totalAttendees} attendees (${requiredCount} required, ${preferredCount} preferred)`)); if (reasons.length > 0) { const reasonsList = $(`

`); reasonsList.append($(`Possible issues:`)); reasons.forEach(reason => { reasonsList.append($(`
`).html(`${reason}`)); }); unassignedElement.append(reasonsList); } unassigned.append($(`
`).append($(`
`).append(unassignedElement))); } else { const conflictStatus = getConflictStatus(assignment.id); const startDate = JSJoda.LocalDate.now().withDayOfYear(timeGrain.dayOfYear ?? timeGrain.day_of_year); const startTime = JSJoda.LocalTime.of(0, 0, 0, 0) .plusMinutes((timeGrain.startingMinuteOfDay ?? timeGrain.starting_minute_of_day)); const startDateTime = JSJoda.LocalDateTime.of(startDate, startTime); const endDateTime = startTime.plusMinutes((meet.durationInGrains ?? meet.duration_in_grains) * 15); meet.requiredAttendances.forEach(attendance => { const byPersonElement = $("
") .append($("
") .append($(conflictStatus.icon)) .append($(`
`).text(meet.topic))); byPersonElement.append($("
").append($(``).text("Required"))); if (meet.preferredAttendances.map(a => a.person).indexOf(attendance.person) >= 0) { byPersonElement.append($("
").append($(``).text("Preferred"))); } byPersonItemData.add({ id: `${assignment.id}-${attendance.person.id}`, group: attendance.person.id, content: byPersonElement.html(), start: startDateTime.toString(), end: endDateTime.toString(), style: `min-height: 50px; ${conflictStatus.style}`, title: conflictStatus.reason || undefined }); }); meet.preferredAttendances.forEach(attendance => { if (meet.requiredAttendances.map(a => a.person).indexOf(attendance.person) === -1) { const byPersonElement = $("
") .append($("
") .append($(conflictStatus.icon)) .append($(`
`).text(meet.topic))); byPersonElement.append($("
").append($(``).text("Preferred"))); byPersonItemData.add({ id: `${assignment.id}-${attendance.person.id}`, group: attendance.person.id, content: byPersonElement.html(), start: startDateTime.toString(), end: endDateTime.toString(), style: `min-height: 50px; ${conflictStatus.style}`, title: conflictStatus.reason || undefined }); } }); } }); byPersonTimeline.setWindow(JSJoda.LocalDateTime.now().plusDays(1).withHour(8).toString(), JSJoda.LocalDateTime.now().plusDays(1).withHour(17).withMinute(45).toString()); } // Click handlers for timeline items byRoomTimeline.on('select', function (properties) { if (properties.items.length > 0) { showMeetingDetails(properties.items[0]); } }); byPersonTimeline.on('select', function (properties) { if (properties.items.length > 0) { // For person view, item id is "assignmentId-personId", extract assignmentId const itemId = properties.items[0]; const assignmentId = itemId.includes('-') ? itemId.split('-').slice(0, -1).join('-') : itemId; showMeetingDetails(assignmentId); } }); function showMeetingDetails(assignmentId) { if (!loadedSchedule) return; // Find the assignment const assignment = loadedSchedule.meetingAssignments.find(a => a.id === assignmentId); if (!assignment) { console.warn('Assignment not found:', assignmentId); return; } // Build lookup maps const meetingMap = new Map(); loadedSchedule.meetings.forEach(m => meetingMap.set(m.id, m)); const roomMap = new Map(); loadedSchedule.rooms.forEach(r => roomMap.set(r.id, r)); const personMap = new Map(); loadedSchedule.people.forEach(p => personMap.set(p.id, p)); // Get meeting and room details const meeting = typeof assignment.meeting === 'string' ? meetingMap.get(assignment.meeting) : assignment.meeting; const room = typeof assignment.room === 'string' ? roomMap.get(assignment.room) : assignment.room; const timeGrain = typeof assignment.startingTimeGrain === 'string' ? loadedSchedule.timeGrains.find(t => t.id === assignment.startingTimeGrain) : assignment.startingTimeGrain; if (!meeting) { console.warn('Meeting not found for assignment:', assignmentId); return; } // Get conflict status const conflictStatus = getConflictStatus(assignmentId); // Build modal content const content = $("#meetingDetailsModalContent"); content.empty(); // Meeting title and status const statusBadge = conflictStatus.status === 'hard' ? 'Hard Conflict' : conflictStatus.status === 'medium' ? 'Medium Issue' : conflictStatus.status === 'soft' ? 'Soft Issue' : 'OK'; content.append($('

').html(meeting.topic + statusBadge)); // Show reason if any if (conflictStatus.reason) { content.append($('
').text(conflictStatus.reason)); } // Details table const detailsTable = $(''); const tbody = $(''); // Duration const durationHours = ((meeting.durationInGrains ?? meeting.duration_in_grains) * 15) / 60; tbody.append($('') .append($('') .append($('') .append($('') .append($('') .append($('
').text('Duration')) .append($('').text(`${durationHours} hour(s) (${(meeting.durationInGrains ?? meeting.duration_in_grains)} time grains)`))); // Room if (room) { tbody.append($('
').text('Room')) .append($('').text(`${room.name} (capacity: ${room.capacity})`))); } else { tbody.append($('
').text('Room')) .append($('').html('Not assigned'))); } // Time if (timeGrain) { const startDate = JSJoda.LocalDate.now().withDayOfYear(timeGrain.dayOfYear ?? timeGrain.day_of_year); const startTime = JSJoda.LocalTime.of(0, 0, 0, 0).plusMinutes((timeGrain.startingMinuteOfDay ?? timeGrain.starting_minute_of_day)); const endTime = startTime.plusMinutes((meeting.durationInGrains ?? meeting.duration_in_grains) * 15); tbody.append($('
').text('Time')) .append($('').text(`${startDate.toString()} ${startTime.toString()} - ${endTime.toString()}`))); } else { tbody.append($('
').text('Time')) .append($('').html('Not scheduled'))); } detailsTable.append(tbody); content.append(detailsTable); // Required Attendees section content.append($('
').text('Required Attendees')); if (meeting.requiredAttendances && meeting.requiredAttendances.length > 0) { const reqList = $('
    '); meeting.requiredAttendances.forEach(att => { const person = att.person?.fullName || (personMap.get(att.person)?.fullName) || att.person; reqList.append($('
  • ') .html(`${person}`)); }); content.append(reqList); } else { content.append($('

    ').text('No required attendees')); } // Preferred Attendees section content.append($('

    ').text('Preferred Attendees')); if (meeting.preferredAttendances && meeting.preferredAttendances.length > 0) { const prefList = $('
      '); meeting.preferredAttendances.forEach(att => { const person = att.person?.fullName || (personMap.get(att.person)?.fullName) || att.person; prefList.append($('
    • ') .html(`${person}`)); }); content.append(prefList); } else { content.append($('

      ').text('No preferred attendees')); } // Conflict details section if (conflictStatus.status !== 'ok' && analyzeCache && analyzeCache.has(assignmentId)) { content.append($('

      ').text('Conflicts')); const conflictList = $('
        '); const violations = analyzeCache.get(assignmentId); // Show hard violations violations.hard.forEach(v => { conflictList.append($('
      • ') .html(`${v.constraint}`)); }); // Show medium violations violations.medium.forEach(v => { conflictList.append($('
      • ') .html(`${v.constraint}`)); }); // Show soft violations violations.soft.forEach(v => { conflictList.append($('
      • ') .html(`${v.constraint}`)); }); content.append(conflictList); } // Update modal title $("#meetingDetailsModalLabel").text("Meeting Details: " + meeting.topic); // Show modal new bootstrap.Modal("#meetingDetailsModal").show(); } function solve() { if (!loadedSchedule) { showError("No schedule data loaded. Please wait for the data to load or refresh the page."); return; } console.log('Sending schedule data for solving:', loadedSchedule); $.post("/schedules", JSON.stringify(loadedSchedule), function (data) { scheduleId = data; refreshSolvingButtons(true); }).fail(function (xhr, ajaxOptions, thrownError) { showError("Start solving failed.", xhr); refreshSolvingButtons(false); }, "text"); } function analyze() { new bootstrap.Modal("#scoreAnalysisModal").show() const scoreAnalysisModalContent = $("#scoreAnalysisModalContent"); scoreAnalysisModalContent.children().remove(); if (loadedSchedule.score == null) { scoreAnalysisModalContent.text("No score to analyze yet, please first press the 'solve' button."); } else { $('#scoreAnalysisScoreLabel').text(`(${loadedSchedule.score})`); $.put("/schedules/analyze", JSON.stringify(loadedSchedule), function (scoreAnalysis) { let constraints = scoreAnalysis.constraints; constraints.sort((a, b) => { let aComponents = getScoreComponents(a.score), bComponents = getScoreComponents(b.score); if (aComponents.hard < 0 && bComponents.hard > 0) return -1; if (aComponents.hard > 0 && bComponents.soft < 0) return 1; if (Math.abs(aComponents.hard) > Math.abs(bComponents.hard)) { return -1; } else { if (aComponents.medium < 0 && bComponents.medium > 0) return -1; if (aComponents.medium > 0 && bComponents.medium < 0) return 1; if (Math.abs(aComponents.medium) > Math.abs(bComponents.medium)) { return -1; } else { if (aComponents.soft < 0 && bComponents.soft > 0) return -1; if (aComponents.soft > 0 && bComponents.soft < 0) return 1; return Math.abs(bComponents.soft) - Math.abs(aComponents.soft); } } }); constraints.map((e) => { let components = getScoreComponents(e.weight); e.type = components.hard != 0 ? 'hard' : (components.medium != 0 ? 'medium' : 'soft'); e.weight = components[e.type]; let scores = getScoreComponents(e.score); e.implicitScore = scores.hard != 0 ? scores.hard : (scores.medium != 0 ? scores.medium : scores.soft); }); scoreAnalysis.constraints = constraints; scoreAnalysisModalContent.children().remove(); scoreAnalysisModalContent.text(""); const analysisTable = $(``).css({textAlign: 'center'}); const analysisTHead = $(``).append($(``) .append($(``)) .append($(``).css({textAlign: 'left'})) .append($(``)) .append($(``)) .append($(``)) .append($(``)) .append($(``))); analysisTable.append(analysisTHead); const analysisTBody = $(``) $.each(scoreAnalysis.constraints, (index, constraintAnalysis) => { let icon = constraintAnalysis.type == "hard" && constraintAnalysis.implicitScore < 0 ? '' : ''; if (!icon) icon = constraintAnalysis.matches.length == 0 ? '' : ''; let row = $(``); row.append($(`
        ConstraintType# MatchesWeightScore
        `).html(icon)) .append($(``).text(constraintAnalysis.name).css({textAlign: 'left'})) .append($(``).text(constraintAnalysis.type)) .append($(``).html(`${constraintAnalysis.matches.length}`)) .append($(``).text(constraintAnalysis.weight)) .append($(``).text(constraintAnalysis.implicitScore)); analysisTBody.append(row); row.append($(``)); }); analysisTable.append(analysisTBody); scoreAnalysisModalContent.append(analysisTable); }).fail(function (xhr, ajaxOptions, thrownError) { showError("Analyze failed.", xhr); }, "text"); } } function getScoreComponents(score) { let components = {hard: 0, medium: 0, soft: 0}; $.each([...score.matchAll(/(-?[0-9]+)(hard|medium|soft)/g)], (i, parts) => { components[parts[2]] = parseInt(parts[1], 10); }); return components; } function refreshSolvingButtons(solving) { if (solving) { $("#solveButton").hide(); $("#stopSolvingButton").show(); $("#solvingSpinner").addClass("active"); if (autoRefreshIntervalId == null) { autoRefreshIntervalId = setInterval(refreshSchedule, 2000); } } else { $("#solveButton").show(); $("#stopSolvingButton").hide(); $("#solvingSpinner").removeClass("active"); if (autoRefreshIntervalId != null) { clearInterval(autoRefreshIntervalId); autoRefreshIntervalId = null; } } } function stopSolving() { $.delete("/schedules/" + scheduleId, function () { refreshSolvingButtons(false); refreshSchedule(); }).fail(function (xhr, ajaxOptions, thrownError) { showError("Stop solving failed.", xhr); }); } function copyTextToClipboard(id) { var text = $("#" + id).text().trim(); var dummy = document.createElement("textarea"); document.body.appendChild(dummy); dummy.value = text; dummy.select(); document.execCommand("copy"); document.body.removeChild(dummy); } function replaceQuickstartSolverForgeAutoHeaderFooter() { const solverforgeHeader = $("header#solverforge-auto-header"); if (solverforgeHeader != null) { solverforgeHeader.css("background-color", "#ffffff"); solverforgeHeader.append( $(`
        `)); } const solverforgeFooter = $("footer#solverforge-auto-footer"); if (solverforgeFooter != null) { solverforgeFooter.append( $(``)); } }