// Maintenance Scheduling Color Palette const COLOR_IDEAL = '#8AE234'; // Green - in ideal window const COLOR_IDEAL_BG = '#8AE23433'; // Green with transparency const COLOR_ACCEPTABLE = '#FCAF3E'; // Orange - after ideal, before due const COLOR_ACCEPTABLE_BG = '#FCAF3E33'; // Orange with transparency const COLOR_OVERDUE = '#EF2929'; // Red - past due date const COLOR_OVERDUE_BG = '#EF292999'; // Red with transparency const BRAND_GREEN = '#10b981'; // SolverForge brand green const HIGHLIGHT_COLOR = 'rgba(99, 102, 241, 0.15)'; // Indigo highlight var autoRefreshIntervalId = null; let highlightedCrewId = null; let demoDataId = null; let scheduleId = null; let loadedSchedule = null; const byCrewPanel = document.getElementById("byCrewPanel"); const byCrewTimelineOptions = { timeAxis: {scale: "day"}, orientation: {axis: "top"}, stack: false, xss: {disabled: true}, // Items are XSS safe through JQuery zoomMin: 3 * 1000 * 60 * 60 * 24 // Three day in milliseconds }; var byCrewGroupData = new vis.DataSet(); var byCrewItemData = new vis.DataSet(); var byCrewTimeline = new vis.Timeline(byCrewPanel, byCrewItemData, byCrewGroupData, byCrewTimelineOptions); const byJobPanel = document.getElementById("byJobPanel"); const byJobTimelineOptions = { timeAxis: {scale: "day"}, orientation: {axis: "top"}, xss: {disabled: true}, // Items are XSS safe through JQuery zoomMin: 3 * 1000 * 60 * 60 * 24 // Three day in milliseconds }; var byJobGroupData = new vis.DataSet(); var byJobItemData = new vis.DataSet(); var byJobTimeline = new vis.Timeline(byJobPanel, byJobItemData, byJobGroupData, byJobTimelineOptions); $(document).ready(function () { replaceQuickstartSolverForgeAutoHeaderFooter(); $("#solveButton").click(function () { solve(); }); $("#stopSolvingButton").click(function () { stopSolving(); }); $("#analyzeButton").click(function () { analyze(); }); // HACK to allow vis-timeline to work within Bootstrap tabs $("#byCrewTab").on('shown.bs.tab', function (event) { byCrewTimeline.redraw(); }) $("#byJobTab").on('shown.bs.tab', function (event) { byJobTimeline.redraw(); }) // Timeline item click handlers byCrewTimeline.on('select', function(properties) { if (properties.items.length > 0) { const itemId = properties.items[0]; // Ignore background items (they have underscore in ID like "job1_readyToIdealEnd") if (!String(itemId).includes('_')) { showJobDetails(itemId); } byCrewTimeline.setSelection([]); // Clear selection } }); byJobTimeline.on('select', function(properties) { if (properties.items.length > 0) { const itemId = properties.items[0]; if (!String(itemId).includes('_')) { showJobDetails(itemId); } byJobTimeline.setSelection([]); } }); setupAjax(); fetchDemoData(); }); 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 fetchDemoData() { $.get("/demo-data", function (data) { data.forEach(item => { $("#testDataButton").append($('' + item + '')); $("#" + item + "TestData").click(function () { switchDataDropDownItemActive(item); scheduleId = null; demoDataId = item; refreshSchedule(); }); }); // load first data set demoDataId = data[0]; switchDataDropDownItemActive(demoDataId); refreshSchedule(); }).fail(function (xhr, ajaxOptions, thrownError) { // disable this page as there is no data let $demo = $("#demo"); $demo.empty(); $demo.html("
No test data available
") }); } function switchDataDropDownItemActive(newItem) { activeCssClass = "active"; $("#testDataButton > a." + activeCssClass).removeClass(activeCssClass); $("#" + newItem + "TestData").addClass(activeCssClass); } function refreshSchedule() { let path = "/schedules/" + scheduleId; if (scheduleId === null) { if (demoDataId === null) { alert("Please select a test data set."); return; } path = "/demo-data/" + demoDataId; } $.getJSON(path, function (schedule) { loadedSchedule = schedule; 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)); const unassignedJobs = $("#unassignedJobs"); unassignedJobs.children().remove(); var unassignedJobsCount = 0; byCrewGroupData.clear(); byJobGroupData.clear(); byCrewItemData.clear(); byJobItemData.clear(); $.each(schedule.crews, (index, crew) => { byCrewGroupData.add({id: crew.id, content: crew.name}); }); $.each(schedule.jobs, (index, job) => { const jobGroupElement = $(``) .append($(``).text(job.name)) .append($(``).text(`${job.durationInDays} workdays`)); byJobGroupData.add({ id: job.id, content: jobGroupElement.html() }); byJobItemData.add({ id: job.id + "_readyToIdealEnd", group: job.id, start: job.minStartDate, end: job.idealEndDate, type: "background", style: `background-color: ${COLOR_IDEAL_BG}` }); byJobItemData.add({ id: job.id + "_idealEndToDue", group: job.id, start: job.idealEndDate, end: job.maxEndDate, type: "background", style: `background-color: ${COLOR_ACCEPTABLE_BG}` }); if (job.crew == null || job.startDate == null) { unassignedJobsCount++; const unassignedJobElement = $(``) .append($(``).text(job.name)) .append($(``).text(`${job.durationInDays} workdays`)) .append($(``).text(`Start: ${job.minStartDate}`)) .append($(``).text(`End: ${job.maxEndDate}`)); const byJobJobElement = $(``) .append($(``).text(`Unassigned`)); $.each(job.tags, (index, tag) => { const color = pickColor(tag); unassignedJobElement.append($(``).text(tag)); byJobJobElement.append($(``).text(tag)); }); const card = $(``).append(unassignedJobElement); card.click(() => showJobDetails(job.id)); unassignedJobs.append($(``).append(card)); byJobItemData.add({ id: job.id, group: job.id, content: byJobJobElement.html(), start: job.minStartDate, end: JSJoda.LocalDate.parse(job.minStartDate).plusDays(job.durationInDays).toString(), style: `background-color: ${COLOR_OVERDUE_BG}` }); } else { const beforeReady = JSJoda.LocalDate.parse(job.startDate).isBefore(JSJoda.LocalDate.parse(job.minStartDate)); const afterDue = JSJoda.LocalDate.parse(job.endDate).isAfter(JSJoda.LocalDate.parse(job.maxEndDate)); const byCrewJobElement = $(``) .append($(``).text(job.name)) .append($(``).text(`${job.durationInDays} workdays`)); const byJobJobElement = $(``) .append($(``).text(job.crew.name)); if (beforeReady) { byCrewJobElement.append($(``).text(`Before ready (too early)`)); byJobJobElement.append($(``).text(`Before ready (too early)`)); } if (afterDue) { byCrewJobElement.append($(``).text(`After due (too late)`)); byJobJobElement.append($(``).text(`After due (too late)`)); } $.each(job.tags, (index, tag) => { const color = pickColor(tag); byCrewJobElement.append($(``).text(tag)); byJobJobElement.append($(``).text(tag)); }); byCrewItemData.add({ id: job.id, group: job.crew.id, content: byCrewJobElement.html(), start: job.startDate, end: job.endDate }); byJobItemData.add({ id: job.id, group: job.id, content: byJobJobElement.html(), start: job.startDate, end: job.endDate, crewId: job.crew.id }); } }); if (unassignedJobsCount === 0) { unassignedJobs.append($(``).text(`There are no unassigned jobs.`)); } byCrewTimeline.setWindow(schedule.workCalendar.fromDate, schedule.workCalendar.toDate); byJobTimeline.setWindow(schedule.workCalendar.fromDate, schedule.workCalendar.toDate); // Render crew table and clear any previous highlighting renderCrewTable(schedule); highlightedCrewId = null; } function solve() { $.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() { analyzeScore(loadedSchedule, "/schedules/analyze"); } 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 = document.getElementById(id).innerText; navigator.clipboard.writeText(text); } function replaceQuickstartSolverForgeAutoHeaderFooter() { const solverforgeHeader = $("header#solverforge-auto-header"); if (solverforgeHeader != null) { solverforgeHeader.css("background-color", "#ffffff"); solverforgeHeader.append( $(`