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>
|
||||
Reference in New Issue
Block a user