init
This commit is contained in:
496
target/classes/META-INF/resources/app.js
Normal file
496
target/classes/META-INF/resources/app.js
Normal file
@@ -0,0 +1,496 @@
|
||||
let autoRefreshIntervalId = null;
|
||||
const zoomMin = 2 * 1000 * 60 * 60 * 24 // 2 day in milliseconds
|
||||
const zoomMax = 4 * 7 * 1000 * 60 * 60 * 24 // 4 weeks in milliseconds
|
||||
|
||||
const UNAVAILABLE_COLOR = '#ef2929' // Tango Scarlet Red
|
||||
const UNDESIRED_COLOR = '#f57900' // Tango Orange
|
||||
const DESIRED_COLOR = '#73d216' // Tango Chameleon
|
||||
|
||||
let demoDataId = null;
|
||||
let scheduleId = null;
|
||||
let loadedSchedule = null;
|
||||
|
||||
const byEmployeePanel = document.getElementById("byEmployeePanel");
|
||||
const byEmployeeTimelineOptions = {
|
||||
timeAxis: {scale: "hour", step: 6},
|
||||
orientation: {axis: "top"},
|
||||
stack: false,
|
||||
xss: {disabled: true}, // Items are XSS safe through JQuery
|
||||
zoomMin: zoomMin,
|
||||
zoomMax: zoomMax,
|
||||
};
|
||||
let byEmployeeGroupDataSet = new vis.DataSet();
|
||||
let byEmployeeItemDataSet = new vis.DataSet();
|
||||
let byEmployeeTimeline = new vis.Timeline(byEmployeePanel, byEmployeeItemDataSet, byEmployeeGroupDataSet, byEmployeeTimelineOptions);
|
||||
|
||||
const byLocationPanel = document.getElementById("byLocationPanel");
|
||||
const byLocationTimelineOptions = {
|
||||
timeAxis: {scale: "hour", step: 6},
|
||||
orientation: {axis: "top"},
|
||||
xss: {disabled: true}, // Items are XSS safe through JQuery
|
||||
zoomMin: zoomMin,
|
||||
zoomMax: zoomMax,
|
||||
};
|
||||
let byLocationGroupDataSet = new vis.DataSet();
|
||||
let byLocationItemDataSet = new vis.DataSet();
|
||||
let byLocationTimeline = new vis.Timeline(byLocationPanel, byLocationItemDataSet, byLocationGroupDataSet, byLocationTimelineOptions);
|
||||
|
||||
let windowStart = JSJoda.LocalDate.now().toString();
|
||||
let windowEnd = JSJoda.LocalDate.parse(windowStart).plusDays(7).toString();
|
||||
|
||||
$(document).ready(function () {
|
||||
replaceQuickstartTimefoldAutoHeaderFooter();
|
||||
|
||||
$("#solveButton").click(function () {
|
||||
solve();
|
||||
});
|
||||
$("#stopSolvingButton").click(function () {
|
||||
stopSolving();
|
||||
});
|
||||
$("#analyzeButton").click(function () {
|
||||
analyze();
|
||||
});
|
||||
// HACK to allow vis-timeline to work within Bootstrap tabs
|
||||
$("#byEmployeeTab").on('shown.bs.tab', function (event) {
|
||||
byEmployeeTimeline.redraw();
|
||||
})
|
||||
$("#byLocationTab").on('shown.bs.tab', function (event) {
|
||||
byLocationTimeline.redraw();
|
||||
})
|
||||
|
||||
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($('<a id="' + item + 'TestData" class="dropdown-item" href="#">' + item + '</a>'));
|
||||
$("#" + item + "TestData").click(function () {
|
||||
switchDataDropDownItemActive(item);
|
||||
scheduleId = null;
|
||||
demoDataId = item;
|
||||
|
||||
refreshSchedule();
|
||||
});
|
||||
});
|
||||
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("<h1><p align=\"center\">No test data available</p></h1>")
|
||||
});
|
||||
}
|
||||
|
||||
function switchDataDropDownItemActive(newItem) {
|
||||
activeCssClass = "active";
|
||||
$("#testDataButton > a." + activeCssClass).removeClass(activeCssClass);
|
||||
$("#" + newItem + "TestData").addClass(activeCssClass);
|
||||
}
|
||||
|
||||
function getShiftColor(shift, employee) {
|
||||
const shiftStart = JSJoda.LocalDateTime.parse(shift.start);
|
||||
const shiftStartDateString = shiftStart.toLocalDate().toString();
|
||||
const shiftEnd = JSJoda.LocalDateTime.parse(shift.end);
|
||||
const shiftEndDateString = shiftEnd.toLocalDate().toString();
|
||||
if (employee.unavailableDates.includes(shiftStartDateString) ||
|
||||
// The contains() check is ignored for a shift end at midnight (00:00:00).
|
||||
(shiftEnd.isAfter(shiftStart.toLocalDate().plusDays(1).atStartOfDay()) &&
|
||||
employee.unavailableDates.includes(shiftEndDateString))) {
|
||||
return UNAVAILABLE_COLOR
|
||||
} else if (employee.undesiredDates.includes(shiftStartDateString) ||
|
||||
// The contains() check is ignored for a shift end at midnight (00:00:00).
|
||||
(shiftEnd.isAfter(shiftStart.toLocalDate().plusDays(1).atStartOfDay()) &&
|
||||
employee.undesiredDates.includes(shiftEndDateString))) {
|
||||
return UNDESIRED_COLOR
|
||||
} else if (employee.desiredDates.includes(shiftStartDateString) ||
|
||||
// The contains() check is ignored for a shift end at midnight (00:00:00).
|
||||
(shiftEnd.isAfter(shiftStart.toLocalDate().plusDays(1).atStartOfDay()) &&
|
||||
employee.desiredDates.includes(shiftEndDateString))) {
|
||||
return DESIRED_COLOR
|
||||
} else {
|
||||
return " #729fcf"; // Tango Sky Blue
|
||||
}
|
||||
}
|
||||
|
||||
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 unassignedShifts = $("#unassignedShifts");
|
||||
const groups = [];
|
||||
|
||||
// Show only first 7 days of draft
|
||||
const scheduleStart = schedule.shifts.map(shift => JSJoda.LocalDateTime.parse(shift.start).toLocalDate()).sort()[0].toString();
|
||||
const scheduleEnd = JSJoda.LocalDate.parse(scheduleStart).plusDays(7).toString();
|
||||
|
||||
windowStart = scheduleStart;
|
||||
windowEnd = scheduleEnd;
|
||||
|
||||
unassignedShifts.children().remove();
|
||||
let unassignedShiftsCount = 0;
|
||||
byEmployeeGroupDataSet.clear();
|
||||
byLocationGroupDataSet.clear();
|
||||
|
||||
byEmployeeItemDataSet.clear();
|
||||
byLocationItemDataSet.clear();
|
||||
|
||||
|
||||
schedule.employees.forEach((employee, index) => {
|
||||
const employeeGroupElement = $('<div class="card-body p-2"/>')
|
||||
.append($(`<h5 class="card-title mb-2"/>)`)
|
||||
.append(employee.name))
|
||||
.append($('<div/>')
|
||||
.append($(employee.skills.map(skill => `<span class="badge me-1 mt-1" style="background-color:#d3d7cf">${skill}</span>`).join(''))));
|
||||
byEmployeeGroupDataSet.add({id: employee.name, content: employeeGroupElement.html()});
|
||||
|
||||
employee.unavailableDates.forEach((rawDate, dateIndex) => {
|
||||
const date = JSJoda.LocalDate.parse(rawDate)
|
||||
const start = date.atStartOfDay().toString();
|
||||
const end = date.plusDays(1).atStartOfDay().toString();
|
||||
const byEmployeeShiftElement = $(`<div/>`)
|
||||
.append($(`<h5 class="card-title mb-1"/>`).text("Unavailable"));
|
||||
byEmployeeItemDataSet.add({
|
||||
id: "employee-" + index + "-unavailability-" + dateIndex, group: employee.name,
|
||||
content: byEmployeeShiftElement.html(),
|
||||
start: start, end: end,
|
||||
type: "background",
|
||||
style: "opacity: 0.5; background-color: " + UNAVAILABLE_COLOR,
|
||||
});
|
||||
});
|
||||
employee.undesiredDates.forEach((rawDate, dateIndex) => {
|
||||
const date = JSJoda.LocalDate.parse(rawDate)
|
||||
const start = date.atStartOfDay().toString();
|
||||
const end = date.plusDays(1).atStartOfDay().toString();
|
||||
const byEmployeeShiftElement = $(`<div/>`)
|
||||
.append($(`<h5 class="card-title mb-1"/>`).text("Undesired"));
|
||||
byEmployeeItemDataSet.add({
|
||||
id: "employee-" + index + "-undesired-" + dateIndex, group: employee.name,
|
||||
content: byEmployeeShiftElement.html(),
|
||||
start: start, end: end,
|
||||
type: "background",
|
||||
style: "opacity: 0.5; background-color: " + UNDESIRED_COLOR,
|
||||
});
|
||||
});
|
||||
employee.desiredDates.forEach((rawDate, dateIndex) => {
|
||||
const date = JSJoda.LocalDate.parse(rawDate)
|
||||
const start = date.atStartOfDay().toString();
|
||||
const end = date.plusDays(1).atStartOfDay().toString();
|
||||
const byEmployeeShiftElement = $(`<div/>`)
|
||||
.append($(`<h5 class="card-title mb-1"/>`).text("Desired"));
|
||||
byEmployeeItemDataSet.add({
|
||||
id: "employee-" + index + "-desired-" + dateIndex, group: employee.name,
|
||||
content: byEmployeeShiftElement.html(),
|
||||
start: start, end: end,
|
||||
type: "background",
|
||||
style: "opacity: 0.5; background-color: " + DESIRED_COLOR,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
schedule.shifts.forEach((shift, index) => {
|
||||
if (groups.indexOf(shift.location) === -1) {
|
||||
groups.push(shift.location);
|
||||
byLocationGroupDataSet.add({
|
||||
id: shift.location,
|
||||
content: shift.location,
|
||||
});
|
||||
}
|
||||
|
||||
if (shift.employee == null) {
|
||||
unassignedShiftsCount++;
|
||||
|
||||
const byLocationShiftElement = $('<div class="card-body p-2"/>')
|
||||
.append($(`<h5 class="card-title mb-2"/>)`)
|
||||
.append("Unassigned"))
|
||||
.append($('<div/>')
|
||||
.append($(`<span class="badge me-1 mt-1" style="background-color:#d3d7cf">${shift.requiredSkill}</span>`)));
|
||||
|
||||
byLocationItemDataSet.add({
|
||||
id: 'shift-' + index, group: shift.location,
|
||||
content: byLocationShiftElement.html(),
|
||||
start: shift.start, end: shift.end,
|
||||
style: "background-color: #EF292999"
|
||||
});
|
||||
} else {
|
||||
const skillColor = (shift.employee.skills.indexOf(shift.requiredSkill) === -1 ? '#ef2929' : '#8ae234');
|
||||
const byEmployeeShiftElement = $('<div class="card-body p-2"/>')
|
||||
.append($(`<h5 class="card-title mb-2"/>)`)
|
||||
.append(shift.location))
|
||||
.append($('<div/>')
|
||||
.append($(`<span class="badge me-1 mt-1" style="background-color:${skillColor}">${shift.requiredSkill}</span>`)));
|
||||
const byLocationShiftElement = $('<div class="card-body p-2"/>')
|
||||
.append($(`<h5 class="card-title mb-2"/>)`)
|
||||
.append(shift.employee.name))
|
||||
.append($('<div/>')
|
||||
.append($(`<span class="badge me-1 mt-1" style="background-color:${skillColor}">${shift.requiredSkill}</span>`)));
|
||||
|
||||
const shiftColor = getShiftColor(shift, shift.employee);
|
||||
byEmployeeItemDataSet.add({
|
||||
id: 'shift-' + index, group: shift.employee.name,
|
||||
content: byEmployeeShiftElement.html(),
|
||||
start: shift.start, end: shift.end,
|
||||
style: "background-color: " + shiftColor
|
||||
});
|
||||
byLocationItemDataSet.add({
|
||||
id: 'shift-' + index, group: shift.location,
|
||||
content: byLocationShiftElement.html(),
|
||||
start: shift.start, end: shift.end,
|
||||
style: "background-color: " + shiftColor
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (unassignedShiftsCount === 0) {
|
||||
unassignedShifts.append($(`<p/>`).text(`There are no unassigned shifts.`));
|
||||
} else {
|
||||
unassignedShifts.append($(`<p/>`).text(`There are ${unassignedShiftsCount} unassigned shifts.`));
|
||||
}
|
||||
byEmployeeTimeline.setWindow(scheduleStart, scheduleEnd);
|
||||
byLocationTimeline.setWindow(scheduleStart, scheduleEnd);
|
||||
}
|
||||
|
||||
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() {
|
||||
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 = $(`<table class="table"/>`).css({textAlign: 'center'});
|
||||
const analysisTHead = $(`<thead/>`).append($(`<tr/>`)
|
||||
.append($(`<th></th>`))
|
||||
.append($(`<th>Constraint</th>`).css({textAlign: 'left'}))
|
||||
.append($(`<th>Type</th>`))
|
||||
.append($(`<th># Matches</th>`))
|
||||
.append($(`<th>Weight</th>`))
|
||||
.append($(`<th>Score</th>`))
|
||||
.append($(`<th></th>`)));
|
||||
analysisTable.append(analysisTHead);
|
||||
const analysisTBody = $(`<tbody/>`)
|
||||
$.each(scoreAnalysis.constraints, (index, constraintAnalysis) => {
|
||||
let icon = constraintAnalysis.type == "hard" && constraintAnalysis.implicitScore < 0 ? '<span class="fas fa-exclamation-triangle" style="color: red"></span>' : '';
|
||||
if (!icon) icon = constraintAnalysis.matches.length == 0 ? '<span class="fas fa-check-circle" style="color: green"></span>' : '';
|
||||
|
||||
let row = $(`<tr/>`);
|
||||
row.append($(`<td/>`).html(icon))
|
||||
.append($(`<td/>`).text(constraintAnalysis.name).css({textAlign: 'left'}))
|
||||
.append($(`<td/>`).text(constraintAnalysis.type))
|
||||
.append($(`<td/>`).html(`<b>${constraintAnalysis.matches.length}</b>`))
|
||||
.append($(`<td/>`).text(constraintAnalysis.weight))
|
||||
.append($(`<td/>`).text(constraintAnalysis.implicitScore));
|
||||
analysisTBody.append(row);
|
||||
row.append($(`<td/>`));
|
||||
});
|
||||
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(/(-?\d*(\.\d+)?)(hard|medium|soft)/g)], (i, parts) => {
|
||||
components[parts[3]] = parseFloat(parts[1], 10);
|
||||
});
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
function refreshSolvingButtons(solving) {
|
||||
if (solving) {
|
||||
$("#solveButton").hide();
|
||||
$("#stopSolvingButton").show();
|
||||
if (autoRefreshIntervalId == null) {
|
||||
autoRefreshIntervalId = setInterval(refreshSchedule, 2000);
|
||||
}
|
||||
} else {
|
||||
$("#solveButton").show();
|
||||
$("#stopSolvingButton").hide();
|
||||
if (autoRefreshIntervalId != null) {
|
||||
clearInterval(autoRefreshIntervalId);
|
||||
autoRefreshIntervalId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refreshSolvingButtons(solving) {
|
||||
if (solving) {
|
||||
$("#solveButton").hide();
|
||||
$("#stopSolvingButton").show();
|
||||
if (autoRefreshIntervalId == null) {
|
||||
autoRefreshIntervalId = setInterval(refreshSchedule, 2000);
|
||||
}
|
||||
} else {
|
||||
$("#solveButton").show();
|
||||
$("#stopSolvingButton").hide();
|
||||
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 replaceQuickstartTimefoldAutoHeaderFooter() {
|
||||
const timefoldHeader = $("header#timefold-auto-header");
|
||||
if (timefoldHeader != null) {
|
||||
timefoldHeader.addClass("bg-black")
|
||||
timefoldHeader.append(
|
||||
$(`<div class="container-fluid">
|
||||
<nav class="navbar sticky-top navbar-expand-lg navbar-dark shadow mb-3">
|
||||
<a class="navbar-brand" href="https://timefold.ai">
|
||||
<img src="/webjars/timefold/img/timefold-logo-horizontal-negative.svg" alt="Timefold logo" width="200">
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item active" id="navUIItem">
|
||||
<button class="nav-link active" id="navUI" data-bs-toggle="pill" data-bs-target="#demo" type="button">Demo UI</button>
|
||||
</li>
|
||||
<li class="nav-item" id="navRestItem">
|
||||
<button class="nav-link" id="navRest" data-bs-toggle="pill" data-bs-target="#rest" type="button">Guide</button>
|
||||
</li>
|
||||
<li class="nav-item" id="navOpenApiItem">
|
||||
<button class="nav-link" id="navOpenApi" data-bs-toggle="pill" data-bs-target="#openapi" type="button">REST API</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Data
|
||||
</button>
|
||||
<div id="testDataButton" class="dropdown-menu" aria-labelledby="dropdownMenuButton"></div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>`));
|
||||
}
|
||||
|
||||
const timefoldFooter = $("footer#timefold-auto-footer");
|
||||
if (timefoldFooter != null) {
|
||||
timefoldFooter.append(
|
||||
$(`<footer class="bg-black text-white-50">
|
||||
<div class="container">
|
||||
<div class="hstack gap-3 p-4">
|
||||
<div class="ms-auto"><a class="text-white" href="https://timefold.ai">Timefold</a></div>
|
||||
<div class="vr"></div>
|
||||
<div><a class="text-white" href="https://timefold.ai/docs">Documentation</a></div>
|
||||
<div class="vr"></div>
|
||||
<div><a class="text-white" href="https://github.com/TimefoldAI/timefold-quickstarts">Code</a></div>
|
||||
<div class="vr"></div>
|
||||
<div class="me-auto"><a class="text-white" href="https://timefold.ai/product/support/">Support</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>`));
|
||||
}
|
||||
}
|
||||
161
target/classes/META-INF/resources/index.html
Normal file
161
target/classes/META-INF/resources/index.html
Normal file
@@ -0,0 +1,161 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||
<title>Employee scheduling - Timefold Solver on Quarkus</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/styles/vis-timeline-graph2d.min.css"
|
||||
integrity="sha256-svzNasPg1yR5gvEaRei2jg+n4Pc3sVyMUWeS6xRAh6U=" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/>
|
||||
<link rel="stylesheet" href="/webjars/timefold/css/timefold-webui.css"/>
|
||||
<style>
|
||||
.vis-time-axis .vis-grid.vis-saturday,
|
||||
.vis-time-axis .vis-grid.vis-sunday {
|
||||
background: #D3D7CFFF;
|
||||
}
|
||||
</style>
|
||||
<link rel="icon" href="/webjars/timefold/img/timefold-favicon.svg" type="image/svg+xml">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header id="timefold-auto-header">
|
||||
<!-- Filled in by app.js -->
|
||||
</header>
|
||||
<div class="tab-content">
|
||||
<div id="demo" class="tab-pane fade show active container-fluid">
|
||||
<div class="sticky-top d-flex justify-content-center align-items-center" aria-live="polite"
|
||||
aria-atomic="true">
|
||||
<div id="notificationPanel" style="position: absolute; top: .5rem;"></div>
|
||||
</div>
|
||||
<h1>Employee scheduling solver</h1>
|
||||
<p>Generate the optimal schedule for your employees.</p>
|
||||
|
||||
<div class="mb-4">
|
||||
<button id="solveButton" type="button" class="btn btn-success">
|
||||
<span class="fas fa-play"></span> Solve
|
||||
</button>
|
||||
<button id="stopSolvingButton" type="button" class="btn btn-danger">
|
||||
<span class="fas fa-stop"></span> Stop solving
|
||||
</button>
|
||||
<span id="unassignedShifts" class="ms-2 align-middle fw-bold"></span>
|
||||
<span id="score" class="score ms-2 align-middle fw-bold">Score: ?</span>
|
||||
<button id="analyzeButton" type="button" class="ms-2 btn btn-secondary">
|
||||
<span class="fas fa-question"></span>
|
||||
</button>
|
||||
|
||||
<div class="float-end">
|
||||
<ul class="nav nav-pills" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="byLocationTab" data-bs-toggle="tab"
|
||||
data-bs-target="#byLocationPanel" type="button" role="tab"
|
||||
aria-controls="byLocationPanel" aria-selected="true">By location
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="byEmployeeTab" data-bs-toggle="tab"
|
||||
data-bs-target="#byEmployeePanel" type="button" role="tab"
|
||||
aria-controls="byEmployeePanel" aria-selected="false">By employee
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 tab-content">
|
||||
<div class="tab-pane fade show active" id="byLocationPanel" role="tabpanel"
|
||||
aria-labelledby="byLocationTab">
|
||||
<div id="locationVisualization"></div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="byEmployeePanel" role="tabpanel" aria-labelledby="byEmployeeTab">
|
||||
<div id="employeeVisualization"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rest" class="tab-pane fade container-fluid">
|
||||
<h1>REST API Guide</h1>
|
||||
|
||||
<h2>Employee Scheduling solver integration via cURL</h2>
|
||||
|
||||
<h3>1. Download demo data</h3>
|
||||
<pre>
|
||||
<button class="btn btn-outline-dark btn-sm float-end"
|
||||
onclick="copyTextToClipboard('curl1')">Copy</button>
|
||||
<code id="curl1">curl -X GET -H 'Accept:application/json' http://localhost:8080/demo-data/SMALL -o sample.json</code>
|
||||
</pre>
|
||||
|
||||
<h3>2. Post the sample data for solving</h3>
|
||||
<p>The POST operation returns a <code>jobId</code> that should be used in subsequent commands.</p>
|
||||
<pre>
|
||||
<button class="btn btn-outline-dark btn-sm float-end"
|
||||
onclick="copyTextToClipboard('curl2')">Copy</button>
|
||||
<code id="curl2">curl -X POST -H 'Content-Type:application/json' http://localhost:8080/schedules -d@sample.json</code>
|
||||
</pre>
|
||||
|
||||
<h3>3. Get the current status and score</h3>
|
||||
<pre>
|
||||
<button class="btn btn-outline-dark btn-sm float-end"
|
||||
onclick="copyTextToClipboard('curl3')">Copy</button>
|
||||
<code id="curl3">curl -X GET -H 'Accept:application/json' http://localhost:8080/schedules/{jobId}/status</code>
|
||||
</pre>
|
||||
|
||||
<h3>4. Get the complete solution</h3>
|
||||
<pre>
|
||||
<button class="btn btn-outline-dark btn-sm float-end"
|
||||
onclick="copyTextToClipboard('curl4')">Copy</button>
|
||||
<code id="curl4">curl -X GET -H 'Accept:application/json' http://localhost:8080/schedules/{jobId}</code>
|
||||
</pre>
|
||||
|
||||
<h3>5. Fetch the analysis of the solution</h3>
|
||||
<pre>
|
||||
<button class="btn btn-outline-dark btn-sm float-end"
|
||||
onclick="copyTextToClipboard('curl5')">Copy</button>
|
||||
<code id="curl5">curl -X PUT -H 'Content-Type:application/json' http://localhost:8080/schedules/analyze -d@solution.json</code>
|
||||
</pre>
|
||||
|
||||
<h3>5. Terminate solving early</h3>
|
||||
<pre>
|
||||
<button class="btn btn-outline-dark btn-sm float-end"
|
||||
onclick="copyTextToClipboard('curl5')">Copy</button>
|
||||
<code id="curl6">curl -X DELETE -H 'Accept:application/json' http://localhost:8080/schedules/{id}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div id="openapi" class="tab-pane fade container-fluid">
|
||||
<h1>REST API Reference</h1>
|
||||
<div class="ratio ratio-1x1">
|
||||
<!-- "scrolling" attribute is obsolete, but e.g. Chrome does not support "overflow:hidden" -->
|
||||
<iframe src="/q/swagger-ui" style="overflow:hidden;" scrolling="no"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer id="timefold-auto-footer"></footer>
|
||||
<div class="modal fadebd-example-modal-lg" id="scoreAnalysisModal" tabindex="-1"
|
||||
aria-labelledby="scoreAnalysisModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="scoreAnalysisModalLabel">Score analysis <span
|
||||
id="scoreAnalysisScoreLabel"></span></h1>
|
||||
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="scoreAnalysisModalContent">
|
||||
<!-- Filled in by app.js -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/webjars/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/webjars/jquery/jquery.min.js"></script>
|
||||
<script src="/webjars/js-joda/dist/js-joda.min.js"></script>
|
||||
<script src="/webjars/timefold/js/timefold-webui.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/standalone/umd/vis-timeline-graph2d.min.js"
|
||||
integrity="sha256-Jy2+UO7rZ2Dgik50z3XrrNpnc5+2PAx9MhL2CicodME=" crossorigin="anonymous"></script>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
43
target/classes/application.properties
Normal file
43
target/classes/application.properties
Normal file
@@ -0,0 +1,43 @@
|
||||
########################
|
||||
# Timefold Solver properties
|
||||
########################
|
||||
|
||||
# The solver runs for 30 seconds. To run for 5 minutes use "5m" and for 2 hours use "2h".
|
||||
quarkus.timefold.solver.termination.spent-limit=30s
|
||||
|
||||
# To change how many solvers to run in parallel
|
||||
# timefold.solver-manager.parallel-solver-count=4
|
||||
|
||||
# Temporary comment this out to detect bugs in your code (lowers performance)
|
||||
# quarkus.timefold.solver.environment-mode=FULL_ASSERT
|
||||
|
||||
# Temporary comment this out to return a feasible solution as soon as possible
|
||||
# quarkus.timefold.solver.termination.best-score-limit=0hard/*soft
|
||||
|
||||
# To see what Timefold is doing, turn on DEBUG or TRACE logging.
|
||||
quarkus.log.category."ai.timefold.solver".level=INFO
|
||||
%test.quarkus.log.category."ai.timefold.solver".level=INFO
|
||||
%prod.quarkus.log.category."ai.timefold.solver".level=INFO
|
||||
|
||||
# XML file for power tweaking, defaults to solverConfig.xml (directly under src/main/resources)
|
||||
# quarkus.timefold.solver-config-xml=org/.../maintenanceScheduleSolverConfig.xml
|
||||
|
||||
########################
|
||||
# Timefold Solver Enterprise properties
|
||||
########################
|
||||
|
||||
# To run increase CPU cores usage per solver
|
||||
%enterprise.quarkus.timefold.solver.move-thread-count=AUTO
|
||||
|
||||
########################
|
||||
# Native build properties
|
||||
########################
|
||||
|
||||
# Enable Swagger UI also in the native mode
|
||||
quarkus.swagger-ui.always-include=true
|
||||
|
||||
########################
|
||||
# Test overrides
|
||||
########################
|
||||
|
||||
%test.quarkus.timefold.solver.termination.spent-limit=10s
|
||||
BIN
target/classes/org/acme/employeescheduling/domain/Employee.class
Normal file
BIN
target/classes/org/acme/employeescheduling/domain/Employee.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/org/acme/employeescheduling/domain/Shift.class
Normal file
BIN
target/classes/org/acme/employeescheduling/domain/Shift.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user