commit 3da9b43ff60629861e1924634ac772244bd80e07 Author: toffe Date: Sat Sep 20 18:51:00 2025 +0200 init diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..4137008 --- /dev/null +++ b/README.MD @@ -0,0 +1,154 @@ +# Employee Scheduling (Java, Quarkus, Maven) + +Schedule shifts to employees, accounting for employee availability and shift skill requirements. + +![Employee Scheduling Screenshot](./employee-scheduling-screenshot.png) + +- [Run the application](#run-the-application) +- [Run the application with Timefold Solver Enterprise Edition](#run-the-application-with-timefold-solver-enterprise-edition) +- [Run the packaged application](#run-the-packaged-application) +- [Run the application in a container](#run-the-application-in-a-container) +- [Run it native](#run-it-native) + +> [!TIP] +> [Check out our off-the-shelf model for Employee Shift Scheduling](https://app.timefold.ai/models/employee-scheduling/v1). This model supports many additional constraints such as skills, pairing employees, fairness and more. + +## Prerequisites + +1. Install Java and Maven, for example with [Sdkman](https://sdkman.io): + + ```sh + $ sdk install java + $ sdk install maven + ``` + +## Run the application + +1. Git clone the timefold-quickstarts repo and navigate to this directory: + + ```sh + $ git clone https://github.com/TimefoldAI/timefold-quickstarts.git + ... + $ cd timefold-quickstarts/java/employee-scheduling + ``` + +2. Start the application with Maven: + + ```sh + $ mvn quarkus:dev + ``` + +3. Visit [http://localhost:8080](http://localhost:8080) in your browser. + +4. Click on the **Solve** button. + +Then try _live coding_: + +- Make some changes in the source code. +- Refresh your browser (F5). + +Notice that those changes are immediately in effect. + +## Run the application with Timefold Solver Enterprise Edition + +For high-scalability use cases, switch to [Timefold Solver Enterprise Edition](https://docs.timefold.ai/timefold-solver/latest/enterprise-edition/enterprise-edition), our commercial offering. +[Contact Timefold](https://timefold.ai/contact) to obtain the credentials required to access our private Enterprise Maven repository. + +1. Create `.m2/settings.xml` in your home directory with the following content: + + ```xml + + ... + + + + timefold-solver-enterprise + my_username + my_password + + + ... + + ``` + + See [Settings Reference](https://maven.apache.org/settings.html) for more information on Maven settings. + +2. Start the application with Maven: + + ```sh + $ mvn clean quarkus:dev -Denterprise + ``` + +3. Visit [http://localhost:8080](http://localhost:8080) in your browser. + +4. Click on the **Solve** button. + +Then try _live coding_: + +- Make some changes in the source code. +- Refresh your browser (F5). + +Notice that those changes are immediately in effect. + +## Run the packaged application + +When you're done iterating in `quarkus:dev` mode, package the application to run as a conventional jar file. + +1. Compile it with Maven: + + ```sh + $ mvn package + ``` + +2. Run it: + + ```sh + $ java -jar ./target/quarkus-app/quarkus-run.jar + ``` + + > **Note** + > To run it on port 8081 instead, add `-Dquarkus.http.port=8081`. + +3. Visit [http://localhost:8080](http://localhost:8080) in your browser. + +4. Click on the **Solve** button. + +## Run the application in a container + +1. Build a container image: + + ```sh + $ mvn package -Dcontainer + ``` + +2. Run a container: + + ```sh + $ docker run -p 8080:8080 $USER/employee-scheduling:1.0-SNAPSHOT + ``` + +## Run it native + +To increase startup performance for serverless deployments, build the application as a native executable: + +1. [Install GraalVM and `gu` install the native-image tool](https://quarkus.io/guides/building-native-image#configuring-graalvm). + +2. Compile it natively. This takes a few minutes: + + ```sh + $ mvn package -Dnative -DskipTests + ``` + +3. Run the native executable: + + ```sh + $ ./target/*-runner + ``` + +4. Visit [http://localhost:8080](http://localhost:8080) in your browser. + +5. Click on the **Solve** button. + +## More information + +Visit [timefold.ai](https://timefold.ai). diff --git a/chat.json b/chat.json new file mode 100644 index 0000000..4ebbc99 --- /dev/null +++ b/chat.json @@ -0,0 +1,41 @@ +{ + "employees": [ + { + "name": "Employee1", + "skills": ["Skill1", "Skill2"], + "unavailableDates": ["2025-01-01", "2025-01-02"], + "undesiredDates": ["2025-01-03"], + "desiredDates": ["2025-01-04"] + }, + { + "name": "Employee2", + "skills": ["Skill2", "Skill3"], + "unavailableDates": ["2025-01-02"], + "undesiredDates": ["2025-01-05"], + "desiredDates": ["2025-01-06"] + } + ], + "shifts": [ + { + "id": "Shift1", + "start": "2025-01-01T08:00:00", + "end": "2025-01-01T16:00:00", + "location": "Location1", + "requiredSkill": "Skill1", + "employee": { + "name": "Employee1" + } + }, + { + "id": "Shift2", + "start": "2025-01-02T08:00:00", + "end": "2025-01-02T16:00:00", + "location": "Location2", + "requiredSkill": "Skill2", + "employee": { + "name": "Employee2" + } + } + ] +} + diff --git a/claude.json b/claude.json new file mode 100644 index 0000000..18e541f --- /dev/null +++ b/claude.json @@ -0,0 +1,162 @@ +{ + "employees": [ + { + "name": "Marie Dupont", + "skills": ["INFIRMIER", "PRELEVEMENT"], + "unavailableDates": ["2024-12-25", "2024-12-26"], + "undesiredDates": ["2024-12-24", "2024-12-31"], + "desiredDates": ["2024-12-20", "2024-12-21"] + }, + { + "name": "Pierre Martin", + "skills": ["MEDECIN", "SUPERVISION"], + "unavailableDates": ["2024-12-30"], + "undesiredDates": ["2024-12-29"], + "desiredDates": ["2024-12-22", "2024-12-23"] + }, + { + "name": "Sophie Bernard", + "skills": ["INFIRMIER", "ACCUEIL"], + "unavailableDates": [], + "undesiredDates": ["2024-12-25"], + "desiredDates": ["2024-12-24", "2024-12-28"] + }, + { + "name": "Jean Leroy", + "skills": ["PRELEVEMENT", "TRANSPORT"], + "unavailableDates": ["2024-12-24", "2024-12-25"], + "undesiredDates": [], + "desiredDates": ["2024-12-27", "2024-12-30"] + }, + { + "name": "Anne Moreau", + "skills": ["MEDECIN", "INFIRMIER"], + "unavailableDates": ["2024-12-26"], + "undesiredDates": ["2024-12-31"], + "desiredDates": ["2024-12-21", "2024-12-29"] + }, + { + "name": "Luc Petit", + "skills": ["ACCUEIL", "TRANSPORT"], + "unavailableDates": [], + "undesiredDates": ["2024-12-20"], + "desiredDates": ["2024-12-25", "2024-12-26"] + } + ], + "shifts": [ + { + "id": "shift_001", + "start": "2024-12-20T08:00:00", + "end": "2024-12-20T16:00:00", + "location": "Centre de collecte - Toulouse", + "requiredSkill": "INFIRMIER", + "employee": null + }, + { + "id": "shift_002", + "start": "2024-12-20T14:00:00", + "end": "2024-12-20T22:00:00", + "location": "Centre de collecte - Toulouse", + "requiredSkill": "PRELEVEMENT", + "employee": null + }, + { + "id": "shift_003", + "start": "2024-12-21T06:00:00", + "end": "2024-12-21T14:00:00", + "location": "Hôpital Purpan", + "requiredSkill": "MEDECIN", + "employee": null + }, + { + "id": "shift_004", + "start": "2024-12-21T08:00:00", + "end": "2024-12-21T12:00:00", + "location": "Centre de collecte - Colomiers", + "requiredSkill": "ACCUEIL", + "employee": null + }, + { + "id": "shift_005", + "start": "2024-12-22T09:00:00", + "end": "2024-12-22T17:00:00", + "location": "Centre de collecte - Blagnac", + "requiredSkill": "INFIRMIER", + "employee": null + }, + { + "id": "shift_006", + "start": "2024-12-22T13:00:00", + "end": "2024-12-22T18:00:00", + "location": "Transport mobile", + "requiredSkill": "TRANSPORT", + "employee": null + }, + { + "id": "shift_007", + "start": "2024-12-23T07:00:00", + "end": "2024-12-23T15:00:00", + "location": "Hôpital Rangueil", + "requiredSkill": "MEDECIN", + "employee": null + }, + { + "id": "shift_008", + "start": "2024-12-23T10:00:00", + "end": "2024-12-23T16:00:00", + "location": "Centre de collecte - Toulouse", + "requiredSkill": "PRELEVEMENT", + "employee": null + }, + { + "id": "shift_009", + "start": "2024-12-24T08:00:00", + "end": "2024-12-24T14:00:00", + "location": "Centre de collecte - Colomiers", + "requiredSkill": "INFIRMIER", + "employee": null + }, + { + "id": "shift_010", + "start": "2024-12-27T09:00:00", + "end": "2024-12-27T17:00:00", + "location": "Centre de collecte - Toulouse", + "requiredSkill": "SUPERVISION", + "employee": null + }, + { + "id": "shift_011", + "start": "2024-12-28T08:00:00", + "end": "2024-12-28T16:00:00", + "location": "Centre de collecte - Blagnac", + "requiredSkill": "ACCUEIL", + "employee": null + }, + { + "id": "shift_012", + "start": "2024-12-29T06:00:00", + "end": "2024-12-29T14:00:00", + "location": "Hôpital Purpan", + "requiredSkill": "MEDECIN", + "employee": null + }, + { + "id": "shift_013", + "start": "2024-12-30T10:00:00", + "end": "2024-12-30T18:00:00", + "location": "Transport mobile", + "requiredSkill": "TRANSPORT", + "employee": null + }, + { + "id": "shift_014", + "start": "2024-12-31T08:00:00", + "end": "2024-12-31T16:00:00", + "location": "Centre de collecte - Toulouse", + "requiredSkill": "INFIRMIER", + "employee": null + } + ], + "score": null, + "solverStatus": null +} diff --git a/employee-scheduling-screenshot.png b/employee-scheduling-screenshot.png new file mode 100644 index 0000000..af99511 Binary files /dev/null and b/employee-scheduling-screenshot.png differ diff --git a/gemini.json b/gemini.json new file mode 100644 index 0000000..03710a6 --- /dev/null +++ b/gemini.json @@ -0,0 +1,60 @@ +{ + "employees": [ + { + "name": "Alice", + "skills": ["Analyse", "Gestion de projet"], + "unavailableDates": ["2025-09-22"], + "undesiredDates": [], + "desiredDates": ["2025-09-24"] + }, + { + "name": "Bob", + "skills": ["Développement", "Analyse"], + "unavailableDates": [], + "undesiredDates": ["2025-09-23"], + "desiredDates": [] + }, + { + "name": "Charlie", + "skills": ["Développement"], + "unavailableDates": [], + "undesiredDates": [], + "desiredDates": [] + } + ], + "shifts": [ + { + "id": "1", + "start": "2025-09-21T09:00:00", + "end": "2025-09-21T17:00:00", + "location": "Bureau de Paris", + "requiredSkill": "Analyse", + "employee": null + }, + { + "id": "2", + "start": "2025-09-22T10:00:00", + "end": "2025-09-22T18:00:00", + "location": "Bureau de Lyon", + "requiredSkill": "Développement", + "employee": null + }, + { + "id": "3", + "start": "2025-09-24T14:00:00", + "end": "2025-09-24T20:00:00", + "location": "Bureau de Paris", + "requiredSkill": "Analyse", + "employee": null + }, + { + "id": "4", + "start": "2025-09-25T08:00:00", + "end": "2025-09-25T16:00:00", + "location": "Bureau de Lyon", + "requiredSkill": "Développement", + "employee": null + } + ] +} + diff --git a/gpt.json b/gpt.json new file mode 100644 index 0000000..9cd5b3a --- /dev/null +++ b/gpt.json @@ -0,0 +1,54 @@ +{ + "employees": [ + { + "name": "Alice", + "skills": ["Nurse", "FirstAid"], + "unavailableDates": ["2025-09-25"], + "undesiredDates": ["2025-09-27"], + "desiredDates": ["2025-09-28"] + }, + { + "name": "Bob", + "skills": ["Doctor"], + "unavailableDates": [], + "undesiredDates": [], + "desiredDates": ["2025-09-25", "2025-09-26"] + }, + { + "name": "Charlie", + "skills": ["Nurse"], + "unavailableDates": ["2025-09-26"], + "undesiredDates": [], + "desiredDates": [] + } + ], + "shifts": [ + { + "id": "S1", + "start": "2025-09-25T08:00:00", + "end": "2025-09-25T16:00:00", + "location": "Hospital A", + "requiredSkill": "Nurse", + "employee": null + }, + { + "id": "S2", + "start": "2025-09-25T16:00:00", + "end": "2025-09-25T23:59:59", + "location": "Hospital A", + "requiredSkill": "Doctor", + "employee": null + }, + { + "id": "S3", + "start": "2025-09-26T08:00:00", + "end": "2025-09-26T16:00:00", + "location": "Hospital B", + "requiredSkill": "Nurse", + "employee": null + } + ], + "score": null, + "solverStatus": null +} + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d81b5ae --- /dev/null +++ b/pom.xml @@ -0,0 +1,245 @@ + + + 4.0.0 + + org.acme + employee-scheduling + 1.0-SNAPSHOT + + + 17 + UTF-8 + + 3.26.2 + 1.26.0 + + 3.14.0 + 3.3.1 + 3.5.3 + + + + + + io.quarkus + quarkus-bom + ${version.io.quarkus} + pom + import + + + ai.timefold.solver + timefold-solver-bom + ${version.ai.timefold.solver} + pom + import + + + + + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-rest-jackson + + + io.quarkus + quarkus-smallrye-openapi + + + ai.timefold.solver + timefold-solver-quarkus + + + ai.timefold.solver + timefold-solver-quarkus-jackson + + + + + io.quarkus + quarkus-web-dependency-locator + + + ai.timefold.solver + timefold-solver-webui + runtime + + + org.webjars + bootstrap + 5.2.3 + runtime + + + org.webjars + jquery + 3.6.4 + runtime + + + org.webjars + font-awesome + 5.15.1 + runtime + + + org.webjars.npm + js-joda + 1.11.0 + runtime + + + + + ai.timefold.solver + timefold-solver-test + test + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + 3.27.4 + test + + + + + + + maven-resources-plugin + ${version.resources.plugin} + + + maven-compiler-plugin + ${version.compiler.plugin} + + + io.quarkus + quarkus-maven-plugin + ${version.io.quarkus} + + + + build + + + + + + maven-surefire-plugin + ${version.surefire.plugin} + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + ${version.surefire.plugin} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + + true + + native + + + + container + + + container + + + + + io.quarkus + quarkus-container-image-jib + + + + true + + + + enterprise + + + enterprise + + + + + timefold-solver-enterprise + Timefold Solver Enterprise Edition + https://timefold.jfrog.io/artifactory/releases/ + + + + + + ai.timefold.solver.enterprise + timefold-solver-enterprise-bom + ${version.ai.timefold.solver} + pom + import + + + + + + ai.timefold.solver.enterprise + timefold-solver-enterprise-quarkus + + + + enterprise + + + + + diff --git a/sample.json b/sample.json new file mode 100644 index 0000000..107741e --- /dev/null +++ b/sample.json @@ -0,0 +1 @@ +{"employees":[{"name":"Elsa Green","skills":["Cardiology","Nurse"],"unavailableDates":["2025-09-30"],"undesiredDates":["2025-09-25"],"desiredDates":[]},{"name":"Elsa Li","skills":["Doctor","Anaesthetics"],"unavailableDates":[],"undesiredDates":["2025-09-22"],"desiredDates":["2025-09-29"]},{"name":"Elsa Watt","skills":["Anaesthetics","Nurse"],"unavailableDates":["2025-10-05"],"undesiredDates":[],"desiredDates":["2025-09-26"]},{"name":"Amy Smith","skills":["Anaesthetics","Nurse"],"unavailableDates":["2025-09-24","2025-10-03"],"undesiredDates":[],"desiredDates":["2025-09-23","2025-10-01"]},{"name":"Hugo King","skills":["Doctor","Cardiology"],"unavailableDates":[],"undesiredDates":[],"desiredDates":[]},{"name":"Amy King","skills":["Cardiology","Nurse"],"unavailableDates":["2025-09-25"],"undesiredDates":[],"desiredDates":[]},{"name":"Dan Poe","skills":["Cardiology","Anaesthetics","Nurse"],"unavailableDates":["2025-10-02"],"undesiredDates":["2025-09-28"],"desiredDates":[]},{"name":"Hugo Watt","skills":["Doctor","Cardiology"],"unavailableDates":[],"undesiredDates":["2025-10-01"],"desiredDates":[]},{"name":"Carl King","skills":["Cardiology","Nurse"],"unavailableDates":[],"undesiredDates":[],"desiredDates":[]},{"name":"Gus Watt","skills":["Anaesthetics","Nurse"],"unavailableDates":["2025-10-04"],"undesiredDates":[],"desiredDates":[]},{"name":"Jay Watt","skills":["Doctor","Cardiology"],"unavailableDates":[],"undesiredDates":["2025-09-28"],"desiredDates":["2025-10-05"]},{"name":"Elsa King","skills":["Doctor","Cardiology","Anaesthetics"],"unavailableDates":[],"undesiredDates":["2025-09-23","2025-10-05"],"desiredDates":[]},{"name":"Dan King","skills":["Cardiology","Nurse"],"unavailableDates":[],"undesiredDates":[],"desiredDates":["2025-09-30"]},{"name":"Carl Green","skills":["Cardiology","Nurse"],"unavailableDates":[],"undesiredDates":["2025-09-24"],"desiredDates":["2025-09-27"]},{"name":"Jay Poe","skills":["Doctor","Anaesthetics"],"unavailableDates":["2025-09-25"],"undesiredDates":["2025-09-27","2025-09-30"],"desiredDates":[]}],"shifts":[{"id":"0","start":"2025-09-22T06:00:00","end":"2025-09-22T14:00:00","location":"Ambulatory care","requiredSkill":"Doctor","employee":null},{"id":"1","start":"2025-09-22T14:00:00","end":"2025-09-22T22:00:00","location":"Ambulatory care","requiredSkill":"Cardiology","employee":null},{"id":"2","start":"2025-09-22T14:00:00","end":"2025-09-22T22:00:00","location":"Ambulatory care","requiredSkill":"Nurse","employee":null},{"id":"3","start":"2025-09-22T06:00:00","end":"2025-09-22T14:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"4","start":"2025-09-22T14:00:00","end":"2025-09-22T22:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"5","start":"2025-09-22T22:00:00","end":"2025-09-23T06:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"6","start":"2025-09-22T06:00:00","end":"2025-09-22T14:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"7","start":"2025-09-22T09:00:00","end":"2025-09-22T17:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"8","start":"2025-09-22T14:00:00","end":"2025-09-22T22:00:00","location":"Pediatric care","requiredSkill":"Cardiology","employee":null},{"id":"9","start":"2025-09-22T22:00:00","end":"2025-09-23T06:00:00","location":"Pediatric care","requiredSkill":"Nurse","employee":null},{"id":"10","start":"2025-09-23T06:00:00","end":"2025-09-23T14:00:00","location":"Ambulatory care","requiredSkill":"Doctor","employee":null},{"id":"11","start":"2025-09-23T14:00:00","end":"2025-09-23T22:00:00","location":"Ambulatory care","requiredSkill":"Anaesthetics","employee":null},{"id":"12","start":"2025-09-23T06:00:00","end":"2025-09-23T14:00:00","location":"Critical care","requiredSkill":"Nurse","employee":null},{"id":"13","start":"2025-09-23T14:00:00","end":"2025-09-23T22:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"14","start":"2025-09-23T22:00:00","end":"2025-09-24T06:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"15","start":"2025-09-23T06:00:00","end":"2025-09-23T14:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"16","start":"2025-09-23T09:00:00","end":"2025-09-23T17:00:00","location":"Pediatric care","requiredSkill":"Cardiology","employee":null},{"id":"17","start":"2025-09-23T14:00:00","end":"2025-09-23T22:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"18","start":"2025-09-23T22:00:00","end":"2025-09-24T06:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"19","start":"2025-09-24T06:00:00","end":"2025-09-24T14:00:00","location":"Ambulatory care","requiredSkill":"Anaesthetics","employee":null},{"id":"20","start":"2025-09-24T14:00:00","end":"2025-09-24T22:00:00","location":"Ambulatory care","requiredSkill":"Cardiology","employee":null},{"id":"21","start":"2025-09-24T06:00:00","end":"2025-09-24T14:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"22","start":"2025-09-24T14:00:00","end":"2025-09-24T22:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"23","start":"2025-09-24T22:00:00","end":"2025-09-25T06:00:00","location":"Critical care","requiredSkill":"Anaesthetics","employee":null},{"id":"24","start":"2025-09-24T06:00:00","end":"2025-09-24T14:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"25","start":"2025-09-24T09:00:00","end":"2025-09-24T17:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"26","start":"2025-09-24T14:00:00","end":"2025-09-24T22:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"27","start":"2025-09-24T22:00:00","end":"2025-09-25T06:00:00","location":"Pediatric care","requiredSkill":"Cardiology","employee":null},{"id":"28","start":"2025-09-25T06:00:00","end":"2025-09-25T14:00:00","location":"Ambulatory care","requiredSkill":"Doctor","employee":null},{"id":"29","start":"2025-09-25T14:00:00","end":"2025-09-25T22:00:00","location":"Ambulatory care","requiredSkill":"Cardiology","employee":null},{"id":"30","start":"2025-09-25T06:00:00","end":"2025-09-25T14:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"31","start":"2025-09-25T14:00:00","end":"2025-09-25T22:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"32","start":"2025-09-25T22:00:00","end":"2025-09-26T06:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"33","start":"2025-09-25T06:00:00","end":"2025-09-25T14:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"34","start":"2025-09-25T09:00:00","end":"2025-09-25T17:00:00","location":"Pediatric care","requiredSkill":"Nurse","employee":null},{"id":"35","start":"2025-09-25T09:00:00","end":"2025-09-25T17:00:00","location":"Pediatric care","requiredSkill":"Nurse","employee":null},{"id":"36","start":"2025-09-25T14:00:00","end":"2025-09-25T22:00:00","location":"Pediatric care","requiredSkill":"Cardiology","employee":null},{"id":"37","start":"2025-09-25T22:00:00","end":"2025-09-26T06:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"38","start":"2025-09-26T06:00:00","end":"2025-09-26T14:00:00","location":"Ambulatory care","requiredSkill":"Nurse","employee":null},{"id":"39","start":"2025-09-26T14:00:00","end":"2025-09-26T22:00:00","location":"Ambulatory care","requiredSkill":"Cardiology","employee":null},{"id":"40","start":"2025-09-26T06:00:00","end":"2025-09-26T14:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"41","start":"2025-09-26T06:00:00","end":"2025-09-26T14:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"42","start":"2025-09-26T14:00:00","end":"2025-09-26T22:00:00","location":"Critical care","requiredSkill":"Anaesthetics","employee":null},{"id":"43","start":"2025-09-26T22:00:00","end":"2025-09-27T06:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"44","start":"2025-09-26T06:00:00","end":"2025-09-26T14:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"45","start":"2025-09-26T09:00:00","end":"2025-09-26T17:00:00","location":"Pediatric care","requiredSkill":"Nurse","employee":null},{"id":"46","start":"2025-09-26T14:00:00","end":"2025-09-26T22:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"47","start":"2025-09-26T22:00:00","end":"2025-09-27T06:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"48","start":"2025-09-27T06:00:00","end":"2025-09-27T14:00:00","location":"Ambulatory care","requiredSkill":"Nurse","employee":null},{"id":"49","start":"2025-09-27T14:00:00","end":"2025-09-27T22:00:00","location":"Ambulatory care","requiredSkill":"Cardiology","employee":null},{"id":"50","start":"2025-09-27T06:00:00","end":"2025-09-27T14:00:00","location":"Critical care","requiredSkill":"Anaesthetics","employee":null},{"id":"51","start":"2025-09-27T14:00:00","end":"2025-09-27T22:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"52","start":"2025-09-27T22:00:00","end":"2025-09-28T06:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"53","start":"2025-09-27T06:00:00","end":"2025-09-27T14:00:00","location":"Pediatric care","requiredSkill":"Nurse","employee":null},{"id":"54","start":"2025-09-27T09:00:00","end":"2025-09-27T17:00:00","location":"Pediatric care","requiredSkill":"Cardiology","employee":null},{"id":"55","start":"2025-09-27T14:00:00","end":"2025-09-27T22:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"56","start":"2025-09-27T22:00:00","end":"2025-09-28T06:00:00","location":"Pediatric care","requiredSkill":"Cardiology","employee":null},{"id":"57","start":"2025-09-28T06:00:00","end":"2025-09-28T14:00:00","location":"Ambulatory care","requiredSkill":"Nurse","employee":null},{"id":"58","start":"2025-09-28T14:00:00","end":"2025-09-28T22:00:00","location":"Ambulatory care","requiredSkill":"Anaesthetics","employee":null},{"id":"59","start":"2025-09-28T06:00:00","end":"2025-09-28T14:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"60","start":"2025-09-28T06:00:00","end":"2025-09-28T14:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"61","start":"2025-09-28T14:00:00","end":"2025-09-28T22:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"62","start":"2025-09-28T22:00:00","end":"2025-09-29T06:00:00","location":"Critical care","requiredSkill":"Anaesthetics","employee":null},{"id":"63","start":"2025-09-28T06:00:00","end":"2025-09-28T14:00:00","location":"Pediatric care","requiredSkill":"Nurse","employee":null},{"id":"64","start":"2025-09-28T09:00:00","end":"2025-09-28T17:00:00","location":"Pediatric care","requiredSkill":"Nurse","employee":null},{"id":"65","start":"2025-09-28T14:00:00","end":"2025-09-28T22:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"66","start":"2025-09-28T22:00:00","end":"2025-09-29T06:00:00","location":"Pediatric care","requiredSkill":"Cardiology","employee":null},{"id":"67","start":"2025-09-29T06:00:00","end":"2025-09-29T14:00:00","location":"Ambulatory care","requiredSkill":"Anaesthetics","employee":null},{"id":"68","start":"2025-09-29T14:00:00","end":"2025-09-29T22:00:00","location":"Ambulatory care","requiredSkill":"Nurse","employee":null},{"id":"69","start":"2025-09-29T06:00:00","end":"2025-09-29T14:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"70","start":"2025-09-29T14:00:00","end":"2025-09-29T22:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"71","start":"2025-09-29T22:00:00","end":"2025-09-30T06:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"72","start":"2025-09-29T06:00:00","end":"2025-09-29T14:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"73","start":"2025-09-29T09:00:00","end":"2025-09-29T17:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"74","start":"2025-09-29T14:00:00","end":"2025-09-29T22:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"75","start":"2025-09-29T22:00:00","end":"2025-09-30T06:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"76","start":"2025-09-30T06:00:00","end":"2025-09-30T14:00:00","location":"Ambulatory care","requiredSkill":"Cardiology","employee":null},{"id":"77","start":"2025-09-30T14:00:00","end":"2025-09-30T22:00:00","location":"Ambulatory care","requiredSkill":"Anaesthetics","employee":null},{"id":"78","start":"2025-09-30T06:00:00","end":"2025-09-30T14:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"79","start":"2025-09-30T14:00:00","end":"2025-09-30T22:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"80","start":"2025-09-30T22:00:00","end":"2025-10-01T06:00:00","location":"Critical care","requiredSkill":"Nurse","employee":null},{"id":"81","start":"2025-09-30T06:00:00","end":"2025-09-30T14:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"82","start":"2025-09-30T09:00:00","end":"2025-09-30T17:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"83","start":"2025-09-30T14:00:00","end":"2025-09-30T22:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"84","start":"2025-09-30T22:00:00","end":"2025-10-01T06:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"85","start":"2025-10-01T06:00:00","end":"2025-10-01T14:00:00","location":"Ambulatory care","requiredSkill":"Cardiology","employee":null},{"id":"86","start":"2025-10-01T14:00:00","end":"2025-10-01T22:00:00","location":"Ambulatory care","requiredSkill":"Nurse","employee":null},{"id":"87","start":"2025-10-01T06:00:00","end":"2025-10-01T14:00:00","location":"Critical care","requiredSkill":"Anaesthetics","employee":null},{"id":"88","start":"2025-10-01T06:00:00","end":"2025-10-01T14:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"89","start":"2025-10-01T14:00:00","end":"2025-10-01T22:00:00","location":"Critical care","requiredSkill":"Anaesthetics","employee":null},{"id":"90","start":"2025-10-01T22:00:00","end":"2025-10-02T06:00:00","location":"Critical care","requiredSkill":"Doctor","employee":null},{"id":"91","start":"2025-10-01T06:00:00","end":"2025-10-01T14:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"92","start":"2025-10-01T09:00:00","end":"2025-10-01T17:00:00","location":"Pediatric care","requiredSkill":"Nurse","employee":null},{"id":"93","start":"2025-10-01T14:00:00","end":"2025-10-01T22:00:00","location":"Pediatric care","requiredSkill":"Cardiology","employee":null},{"id":"94","start":"2025-10-01T22:00:00","end":"2025-10-02T06:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"95","start":"2025-10-02T06:00:00","end":"2025-10-02T14:00:00","location":"Ambulatory care","requiredSkill":"Nurse","employee":null},{"id":"96","start":"2025-10-02T14:00:00","end":"2025-10-02T22:00:00","location":"Ambulatory care","requiredSkill":"Anaesthetics","employee":null},{"id":"97","start":"2025-10-02T06:00:00","end":"2025-10-02T14:00:00","location":"Critical care","requiredSkill":"Anaesthetics","employee":null},{"id":"98","start":"2025-10-02T14:00:00","end":"2025-10-02T22:00:00","location":"Critical care","requiredSkill":"Nurse","employee":null},{"id":"99","start":"2025-10-02T22:00:00","end":"2025-10-03T06:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"100","start":"2025-10-02T06:00:00","end":"2025-10-02T14:00:00","location":"Pediatric care","requiredSkill":"Cardiology","employee":null},{"id":"101","start":"2025-10-02T09:00:00","end":"2025-10-02T17:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"102","start":"2025-10-02T14:00:00","end":"2025-10-02T22:00:00","location":"Pediatric care","requiredSkill":"Cardiology","employee":null},{"id":"103","start":"2025-10-02T22:00:00","end":"2025-10-03T06:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"104","start":"2025-10-03T06:00:00","end":"2025-10-03T14:00:00","location":"Ambulatory care","requiredSkill":"Nurse","employee":null},{"id":"105","start":"2025-10-03T14:00:00","end":"2025-10-03T22:00:00","location":"Ambulatory care","requiredSkill":"Anaesthetics","employee":null},{"id":"106","start":"2025-10-03T06:00:00","end":"2025-10-03T14:00:00","location":"Critical care","requiredSkill":"Anaesthetics","employee":null},{"id":"107","start":"2025-10-03T14:00:00","end":"2025-10-03T22:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"108","start":"2025-10-03T14:00:00","end":"2025-10-03T22:00:00","location":"Critical care","requiredSkill":"Nurse","employee":null},{"id":"109","start":"2025-10-03T22:00:00","end":"2025-10-04T06:00:00","location":"Critical care","requiredSkill":"Nurse","employee":null},{"id":"110","start":"2025-10-03T06:00:00","end":"2025-10-03T14:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"111","start":"2025-10-03T09:00:00","end":"2025-10-03T17:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"112","start":"2025-10-03T14:00:00","end":"2025-10-03T22:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"113","start":"2025-10-03T22:00:00","end":"2025-10-04T06:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"114","start":"2025-10-04T06:00:00","end":"2025-10-04T14:00:00","location":"Ambulatory care","requiredSkill":"Cardiology","employee":null},{"id":"115","start":"2025-10-04T14:00:00","end":"2025-10-04T22:00:00","location":"Ambulatory care","requiredSkill":"Cardiology","employee":null},{"id":"116","start":"2025-10-04T06:00:00","end":"2025-10-04T14:00:00","location":"Critical care","requiredSkill":"Nurse","employee":null},{"id":"117","start":"2025-10-04T14:00:00","end":"2025-10-04T22:00:00","location":"Critical care","requiredSkill":"Anaesthetics","employee":null},{"id":"118","start":"2025-10-04T22:00:00","end":"2025-10-05T06:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"119","start":"2025-10-04T06:00:00","end":"2025-10-04T14:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"120","start":"2025-10-04T09:00:00","end":"2025-10-04T17:00:00","location":"Pediatric care","requiredSkill":"Nurse","employee":null},{"id":"121","start":"2025-10-04T14:00:00","end":"2025-10-04T22:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"122","start":"2025-10-04T22:00:00","end":"2025-10-05T06:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null},{"id":"123","start":"2025-10-05T06:00:00","end":"2025-10-05T14:00:00","location":"Ambulatory care","requiredSkill":"Anaesthetics","employee":null},{"id":"124","start":"2025-10-05T14:00:00","end":"2025-10-05T22:00:00","location":"Ambulatory care","requiredSkill":"Nurse","employee":null},{"id":"125","start":"2025-10-05T14:00:00","end":"2025-10-05T22:00:00","location":"Ambulatory care","requiredSkill":"Doctor","employee":null},{"id":"126","start":"2025-10-05T06:00:00","end":"2025-10-05T14:00:00","location":"Critical care","requiredSkill":"Anaesthetics","employee":null},{"id":"127","start":"2025-10-05T14:00:00","end":"2025-10-05T22:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"128","start":"2025-10-05T22:00:00","end":"2025-10-06T06:00:00","location":"Critical care","requiredSkill":"Cardiology","employee":null},{"id":"129","start":"2025-10-05T06:00:00","end":"2025-10-05T14:00:00","location":"Pediatric care","requiredSkill":"Cardiology","employee":null},{"id":"130","start":"2025-10-05T09:00:00","end":"2025-10-05T17:00:00","location":"Pediatric care","requiredSkill":"Nurse","employee":null},{"id":"131","start":"2025-10-05T14:00:00","end":"2025-10-05T22:00:00","location":"Pediatric care","requiredSkill":"Doctor","employee":null},{"id":"132","start":"2025-10-05T22:00:00","end":"2025-10-06T06:00:00","location":"Pediatric care","requiredSkill":"Anaesthetics","employee":null}],"score":null,"solverStatus":null} \ No newline at end of file diff --git a/solution.json b/solution.json new file mode 100644 index 0000000..bf2dd3b --- /dev/null +++ b/solution.json @@ -0,0 +1 @@ +{"employees":[{"name":"Marie Dupont","skills":["PRELEVEMENT","INFIRMIER"],"unavailableDates":["2024-12-26","2024-12-25"],"undesiredDates":["2024-12-31","2024-12-24"],"desiredDates":["2024-12-21","2024-12-20"]},{"name":"Pierre Martin","skills":["MEDECIN","SUPERVISION"],"unavailableDates":["2024-12-30"],"undesiredDates":["2024-12-29"],"desiredDates":["2024-12-23","2024-12-22"]},{"name":"Sophie Bernard","skills":["ACCUEIL","INFIRMIER"],"unavailableDates":[],"undesiredDates":["2024-12-25"],"desiredDates":["2024-12-28","2024-12-24"]},{"name":"Jean Leroy","skills":["PRELEVEMENT","TRANSPORT"],"unavailableDates":["2024-12-25","2024-12-24"],"undesiredDates":[],"desiredDates":["2024-12-30","2024-12-27"]},{"name":"Anne Moreau","skills":["MEDECIN","INFIRMIER"],"unavailableDates":["2024-12-26"],"undesiredDates":["2024-12-31"],"desiredDates":["2024-12-29","2024-12-21"]},{"name":"Luc Petit","skills":["TRANSPORT","ACCUEIL"],"unavailableDates":[],"undesiredDates":["2024-12-20"],"desiredDates":["2024-12-26","2024-12-25"]}],"shifts":[{"id":"shift_001","start":"2024-12-20T08:00:00","end":"2024-12-20T16:00:00","location":"Centre de collecte - Toulouse","requiredSkill":"INFIRMIER","employee":{"name":"Marie Dupont","skills":["PRELEVEMENT","INFIRMIER"],"unavailableDates":["2024-12-26","2024-12-25"],"undesiredDates":["2024-12-31","2024-12-24"],"desiredDates":["2024-12-21","2024-12-20"]}},{"id":"shift_002","start":"2024-12-20T14:00:00","end":"2024-12-20T22:00:00","location":"Centre de collecte - Toulouse","requiredSkill":"PRELEVEMENT","employee":{"name":"Jean Leroy","skills":["PRELEVEMENT","TRANSPORT"],"unavailableDates":["2024-12-25","2024-12-24"],"undesiredDates":[],"desiredDates":["2024-12-30","2024-12-27"]}},{"id":"shift_003","start":"2024-12-21T06:00:00","end":"2024-12-21T14:00:00","location":"Hôpital Purpan","requiredSkill":"MEDECIN","employee":{"name":"Anne Moreau","skills":["MEDECIN","INFIRMIER"],"unavailableDates":["2024-12-26"],"undesiredDates":["2024-12-31"],"desiredDates":["2024-12-29","2024-12-21"]}},{"id":"shift_004","start":"2024-12-21T08:00:00","end":"2024-12-21T12:00:00","location":"Centre de collecte - Colomiers","requiredSkill":"ACCUEIL","employee":{"name":"Luc Petit","skills":["TRANSPORT","ACCUEIL"],"unavailableDates":[],"undesiredDates":["2024-12-20"],"desiredDates":["2024-12-26","2024-12-25"]}},{"id":"shift_005","start":"2024-12-22T09:00:00","end":"2024-12-22T17:00:00","location":"Centre de collecte - Blagnac","requiredSkill":"INFIRMIER","employee":{"name":"Marie Dupont","skills":["PRELEVEMENT","INFIRMIER"],"unavailableDates":["2024-12-26","2024-12-25"],"undesiredDates":["2024-12-31","2024-12-24"],"desiredDates":["2024-12-21","2024-12-20"]}},{"id":"shift_006","start":"2024-12-22T13:00:00","end":"2024-12-22T18:00:00","location":"Transport mobile","requiredSkill":"TRANSPORT","employee":{"name":"Luc Petit","skills":["TRANSPORT","ACCUEIL"],"unavailableDates":[],"undesiredDates":["2024-12-20"],"desiredDates":["2024-12-26","2024-12-25"]}},{"id":"shift_007","start":"2024-12-23T07:00:00","end":"2024-12-23T15:00:00","location":"Hôpital Rangueil","requiredSkill":"MEDECIN","employee":{"name":"Pierre Martin","skills":["MEDECIN","SUPERVISION"],"unavailableDates":["2024-12-30"],"undesiredDates":["2024-12-29"],"desiredDates":["2024-12-23","2024-12-22"]}},{"id":"shift_008","start":"2024-12-23T10:00:00","end":"2024-12-23T16:00:00","location":"Centre de collecte - Toulouse","requiredSkill":"PRELEVEMENT","employee":{"name":"Jean Leroy","skills":["PRELEVEMENT","TRANSPORT"],"unavailableDates":["2024-12-25","2024-12-24"],"undesiredDates":[],"desiredDates":["2024-12-30","2024-12-27"]}},{"id":"shift_009","start":"2024-12-24T08:00:00","end":"2024-12-24T14:00:00","location":"Centre de collecte - Colomiers","requiredSkill":"INFIRMIER","employee":{"name":"Sophie Bernard","skills":["ACCUEIL","INFIRMIER"],"unavailableDates":[],"undesiredDates":["2024-12-25"],"desiredDates":["2024-12-28","2024-12-24"]}},{"id":"shift_010","start":"2024-12-27T09:00:00","end":"2024-12-27T17:00:00","location":"Centre de collecte - Toulouse","requiredSkill":"SUPERVISION","employee":{"name":"Pierre Martin","skills":["MEDECIN","SUPERVISION"],"unavailableDates":["2024-12-30"],"undesiredDates":["2024-12-29"],"desiredDates":["2024-12-23","2024-12-22"]}},{"id":"shift_011","start":"2024-12-28T08:00:00","end":"2024-12-28T16:00:00","location":"Centre de collecte - Blagnac","requiredSkill":"ACCUEIL","employee":{"name":"Sophie Bernard","skills":["ACCUEIL","INFIRMIER"],"unavailableDates":[],"undesiredDates":["2024-12-25"],"desiredDates":["2024-12-28","2024-12-24"]}},{"id":"shift_012","start":"2024-12-29T06:00:00","end":"2024-12-29T14:00:00","location":"Hôpital Purpan","requiredSkill":"MEDECIN","employee":{"name":"Anne Moreau","skills":["MEDECIN","INFIRMIER"],"unavailableDates":["2024-12-26"],"undesiredDates":["2024-12-31"],"desiredDates":["2024-12-29","2024-12-21"]}},{"id":"shift_013","start":"2024-12-30T10:00:00","end":"2024-12-30T18:00:00","location":"Transport mobile","requiredSkill":"TRANSPORT","employee":{"name":"Jean Leroy","skills":["PRELEVEMENT","TRANSPORT"],"unavailableDates":["2024-12-25","2024-12-24"],"undesiredDates":[],"desiredDates":["2024-12-30","2024-12-27"]}},{"id":"shift_014","start":"2024-12-31T08:00:00","end":"2024-12-31T16:00:00","location":"Centre de collecte - Toulouse","requiredSkill":"INFIRMIER","employee":{"name":"Sophie Bernard","skills":["ACCUEIL","INFIRMIER"],"unavailableDates":[],"undesiredDates":["2024-12-25"],"desiredDates":["2024-12-28","2024-12-24"]}}],"score":"0hard/3238.84530soft","solverStatus":"NOT_SOLVING"} \ No newline at end of file diff --git a/src/main/java/org/acme/employeescheduling/domain/Employee.java b/src/main/java/org/acme/employeescheduling/domain/Employee.java new file mode 100644 index 0000000..0f2ff5d --- /dev/null +++ b/src/main/java/org/acme/employeescheduling/domain/Employee.java @@ -0,0 +1,91 @@ +package org.acme.employeescheduling.domain; + +import java.time.LocalDate; +import java.util.Objects; +import java.util.Set; + +import ai.timefold.solver.core.api.domain.lookup.PlanningId; + +public class Employee { + @PlanningId + private String name; + private Set skills; + + private Set unavailableDates; + private Set undesiredDates; + private Set desiredDates; + + public Employee() { + + } + + public Employee(String name, Set skills, + Set unavailableDates, Set undesiredDates, Set desiredDates) { + this.name = name; + this.skills = skills; + this.unavailableDates = unavailableDates; + this.undesiredDates = undesiredDates; + this.desiredDates = desiredDates; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getSkills() { + return skills; + } + + public void setSkills(Set skills) { + this.skills = skills; + } + + public Set getUnavailableDates() { + return unavailableDates; + } + + public void setUnavailableDates(Set unavailableDates) { + this.unavailableDates = unavailableDates; + } + + public Set getUndesiredDates() { + return undesiredDates; + } + + public void setUndesiredDates(Set undesiredDates) { + this.undesiredDates = undesiredDates; + } + + public Set getDesiredDates() { + return desiredDates; + } + + public void setDesiredDates(Set desiredDates) { + this.desiredDates = desiredDates; + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Employee employee)) { + return false; + } + return Objects.equals(getName(), employee.getName()); + } + + @Override + public int hashCode() { + return getName().hashCode(); + } +} diff --git a/src/main/java/org/acme/employeescheduling/domain/EmployeeSchedule.java b/src/main/java/org/acme/employeescheduling/domain/EmployeeSchedule.java new file mode 100644 index 0000000..dc3ed1e --- /dev/null +++ b/src/main/java/org/acme/employeescheduling/domain/EmployeeSchedule.java @@ -0,0 +1,72 @@ +package org.acme.employeescheduling.domain; + +import java.util.List; + +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.solution.PlanningScore; +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.hardsoftbigdecimal.HardSoftBigDecimalScore; +import ai.timefold.solver.core.api.solver.SolverStatus; + +@PlanningSolution +public class EmployeeSchedule { + + @ProblemFactCollectionProperty + @ValueRangeProvider + private List employees; + + @PlanningEntityCollectionProperty + private List shifts; + + @PlanningScore + private HardSoftBigDecimalScore score; + + private SolverStatus solverStatus; + + // No-arg constructor required for Timefold + public EmployeeSchedule() {} + + public EmployeeSchedule(List employees, List shifts) { + this.employees = employees; + this.shifts = shifts; + } + + public EmployeeSchedule(HardSoftBigDecimalScore score, SolverStatus solverStatus) { + this.score = score; + this.solverStatus = solverStatus; + } + + public List getEmployees() { + return employees; + } + + public void setEmployees(List employees) { + this.employees = employees; + } + + public List getShifts() { + return shifts; + } + + public void setShifts(List shifts) { + this.shifts = shifts; + } + + public HardSoftBigDecimalScore getScore() { + return score; + } + + public void setScore(HardSoftBigDecimalScore score) { + this.score = score; + } + + public SolverStatus getSolverStatus() { + return solverStatus; + } + + public void setSolverStatus(SolverStatus solverStatus) { + this.solverStatus = solverStatus; + } +} diff --git a/src/main/java/org/acme/employeescheduling/domain/Shift.java b/src/main/java/org/acme/employeescheduling/domain/Shift.java new file mode 100644 index 0000000..74cb018 --- /dev/null +++ b/src/main/java/org/acme/employeescheduling/domain/Shift.java @@ -0,0 +1,133 @@ +package org.acme.employeescheduling.domain; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import java.util.Objects; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.lookup.PlanningId; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; + +@PlanningEntity +public class Shift { + @PlanningId + private String id; + + private LocalDateTime start; + private LocalDateTime end; + + private String location; + private String requiredSkill; + + @PlanningVariable + private Employee employee; + + public Shift() { + } + + public Shift(LocalDateTime start, LocalDateTime end, String location, String requiredSkill) { + this(start, end, location, requiredSkill, null); + } + + public Shift(LocalDateTime start, LocalDateTime end, String location, String requiredSkill, Employee employee) { + this(null, start, end, location, requiredSkill, employee); + } + + public Shift(String id, LocalDateTime start, LocalDateTime end, String location, String requiredSkill, Employee employee) { + this.id = id; + this.start = start; + this.end = end; + this.location = location; + this.requiredSkill = requiredSkill; + this.employee = employee; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public LocalDateTime getStart() { + return start; + } + + public void setStart(LocalDateTime start) { + this.start = start; + } + + public LocalDateTime getEnd() { + return end; + } + + public void setEnd(LocalDateTime end) { + this.end = end; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public String getRequiredSkill() { + return requiredSkill; + } + + public void setRequiredSkill(String requiredSkill) { + this.requiredSkill = requiredSkill; + } + + public Employee getEmployee() { + return employee; + } + + public void setEmployee(Employee employee) { + this.employee = employee; + } + + public boolean isOverlappingWithDate(LocalDate date) { + return getStart().toLocalDate().equals(date) || getEnd().toLocalDate().equals(date); + } + + public int getOverlappingDurationInMinutes(LocalDate date) { + LocalDateTime startDateTime = LocalDateTime.of(date, LocalTime.MIN); + LocalDateTime endDateTime = LocalDateTime.of(date, LocalTime.MAX); + return getOverlappingDurationInMinutes(startDateTime, endDateTime, getStart(), getEnd()); + } + + private int getOverlappingDurationInMinutes(LocalDateTime firstStartDateTime, LocalDateTime firstEndDateTime, + LocalDateTime secondStartDateTime, LocalDateTime secondEndDateTime) { + LocalDateTime maxStartTime = firstStartDateTime.isAfter(secondStartDateTime) ? firstStartDateTime : secondStartDateTime; + LocalDateTime minEndTime = firstEndDateTime.isBefore(secondEndDateTime) ? firstEndDateTime : secondEndDateTime; + long minutes = maxStartTime.until(minEndTime, ChronoUnit.MINUTES); + return minutes > 0 ? (int) minutes : 0; + } + + @Override + public String toString() { + return location + " " + start + "-" + end; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Shift shift)) { + return false; + } + return Objects.equals(getId(), shift.getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/src/main/java/org/acme/employeescheduling/rest/DemoDataGenerator.java b/src/main/java/org/acme/employeescheduling/rest/DemoDataGenerator.java new file mode 100644 index 0000000..5230bfe --- /dev/null +++ b/src/main/java/org/acme/employeescheduling/rest/DemoDataGenerator.java @@ -0,0 +1,240 @@ +package org.acme.employeescheduling.rest; + +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.TemporalAdjusters; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.acme.employeescheduling.domain.Employee; +import org.acme.employeescheduling.domain.EmployeeSchedule; +import org.acme.employeescheduling.domain.Shift; + +@ApplicationScoped +public class DemoDataGenerator { + public enum DemoData { + SMALL(new DemoDataParameters( + List.of("Ambulatory care", "Critical care", "Pediatric care"), + List.of("Doctor", "Nurse"), + List.of("Anaesthetics", "Cardiology"), + 14, + 15, + List.of(new CountDistribution(1, 3), + new CountDistribution(2, 1) + ), + List.of(new CountDistribution(1, 0.9), + new CountDistribution(2, 0.1) + ), + List.of(new CountDistribution(1, 4), + new CountDistribution(2, 3), + new CountDistribution(3, 2), + new CountDistribution(4, 1) + ), + 0 + )), + LARGE(new DemoDataParameters( + List.of("Ambulatory care", + "Neurology", + "Critical care", + "Pediatric care", + "Surgery", + "Radiology", + "Outpatient"), + List.of("Doctor", "Nurse"), + List.of("Anaesthetics", "Cardiology", "Radiology"), + 28, + 50, + List.of(new CountDistribution(1, 3), + new CountDistribution(2, 1) + ), + List.of(new CountDistribution(1, 0.5), + new CountDistribution(2, 0.3), + new CountDistribution(3, 0.2) + ), + List.of(new CountDistribution(5, 4), + new CountDistribution(10, 3), + new CountDistribution(15, 2), + new CountDistribution(20, 1) + ), + 0 + )); + + private final DemoDataParameters parameters; + + DemoData(DemoDataParameters parameters) { + this.parameters = parameters; + } + + public DemoDataParameters getParameters() { + return parameters; + } + } + + public record CountDistribution(int count, double weight) {} + + public record DemoDataParameters(List locations, + List requiredSkills, + List optionalSkills, + int daysInSchedule, + int employeeCount, + List optionalSkillDistribution, + List shiftCountDistribution, + List availabilityCountDistribution, + int randomSeed) {} + + private static final String[] FIRST_NAMES = { "Amy", "Beth", "Carl", "Dan", "Elsa", "Flo", "Gus", "Hugo", "Ivy", "Jay" }; + private static final String[] LAST_NAMES = { "Cole", "Fox", "Green", "Jones", "King", "Li", "Poe", "Rye", "Smith", "Watt" }; + private static final Duration SHIFT_LENGTH = Duration.ofHours(8); + private static final LocalTime MORNING_SHIFT_START_TIME = LocalTime.of(6, 0); + private static final LocalTime DAY_SHIFT_START_TIME = LocalTime.of(9, 0); + private static final LocalTime AFTERNOON_SHIFT_START_TIME = LocalTime.of(14, 0); + private static final LocalTime NIGHT_SHIFT_START_TIME = LocalTime.of(22, 0); + + static final LocalTime[][] SHIFT_START_TIMES_COMBOS = { + { MORNING_SHIFT_START_TIME, AFTERNOON_SHIFT_START_TIME }, + { MORNING_SHIFT_START_TIME, AFTERNOON_SHIFT_START_TIME, NIGHT_SHIFT_START_TIME }, + { MORNING_SHIFT_START_TIME, DAY_SHIFT_START_TIME, AFTERNOON_SHIFT_START_TIME, NIGHT_SHIFT_START_TIME }, + }; + + Map> locationToShiftStartTimeListMap = new HashMap<>(); + + public EmployeeSchedule generateDemoData(DemoData demoData) { + return generateDemoData(demoData.getParameters()); + } + + public EmployeeSchedule generateDemoData(DemoDataParameters parameters) { + EmployeeSchedule employeeSchedule = new EmployeeSchedule(); + + LocalDate startDate = LocalDate.now().with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY)); + + Random random = new Random(parameters.randomSeed); + + int shiftTemplateIndex = 0; + for (String location : parameters.locations) { + locationToShiftStartTimeListMap.put(location, List.of(SHIFT_START_TIMES_COMBOS[shiftTemplateIndex])); + shiftTemplateIndex = (shiftTemplateIndex + 1) % SHIFT_START_TIMES_COMBOS.length; + } + + List namePermutations = joinAllCombinations(FIRST_NAMES, LAST_NAMES); + Collections.shuffle(namePermutations, random); + + List employees = new ArrayList<>(); + for (int i = 0; i < parameters.employeeCount; i++) { + Set skills = pickSubset(parameters.optionalSkills, random, parameters.optionalSkillDistribution); + skills.add(pickRandom(parameters.requiredSkills, random)); + Employee employee = new Employee(namePermutations.get(i), skills, new LinkedHashSet<>(), new LinkedHashSet<>(), new LinkedHashSet<>()); + employees.add(employee); + } + employeeSchedule.setEmployees(employees); + + List shifts = new LinkedList<>(); + for (int i = 0; i < parameters.daysInSchedule; i++) { + Set employeesWithAvailabilitiesOnDay = pickSubset(employees, random, + parameters.availabilityCountDistribution); + LocalDate date = startDate.plusDays(i); + for (Employee employee : employeesWithAvailabilitiesOnDay) { + switch (random.nextInt(3)) { + case 0 -> employee.getUnavailableDates().add(date); + case 1 -> employee.getUndesiredDates().add(date); + case 2 -> employee.getDesiredDates().add(date); + } + } + shifts.addAll(generateShiftsForDay(parameters, date, random)); + } + AtomicInteger countShift = new AtomicInteger(); + shifts.forEach(s -> s.setId(Integer.toString(countShift.getAndIncrement()))); + employeeSchedule.setShifts(shifts); + + return employeeSchedule; + } + + private List generateShiftsForDay(DemoDataParameters parameters, LocalDate date, Random random) { + List shifts = new LinkedList<>(); + for (String location : parameters.locations) { + List shiftStartTimes = locationToShiftStartTimeListMap.get(location); + for (LocalTime shiftStartTime : shiftStartTimes) { + LocalDateTime shiftStartDateTime = date.atTime(shiftStartTime); + LocalDateTime shiftEndDateTime = shiftStartDateTime.plus(SHIFT_LENGTH); + shifts.addAll(generateShiftForTimeslot(parameters, shiftStartDateTime, shiftEndDateTime, location, random)); + } + } + return shifts; + } + + private List generateShiftForTimeslot(DemoDataParameters parameters, + LocalDateTime timeslotStart, LocalDateTime timeslotEnd, String location, + Random random) { + var shiftCount = pickCount(random, parameters.shiftCountDistribution); + + List shifts = new LinkedList<>(); + for (int i = 0; i < shiftCount; i++) { + String requiredSkill; + if (random.nextBoolean()) { + requiredSkill = pickRandom(parameters.requiredSkills, random); + } else { + requiredSkill = pickRandom(parameters.optionalSkills, random); + } + shifts.add(new Shift(timeslotStart, timeslotEnd, location, requiredSkill)); + } + return shifts; + } + + private T pickRandom(List source, Random random) { + return source.get(random.nextInt(source.size())); + } + + private int pickCount(Random random, List countDistribution) { + double probabilitySum = 0; + for (var possibility : countDistribution) { + probabilitySum += possibility.weight; + } + var choice = random.nextDouble(probabilitySum); + int numOfItems = 0; + while (choice >= countDistribution.get(numOfItems).weight) { + choice -= countDistribution.get(numOfItems).weight; + numOfItems++; + } + return countDistribution.get(numOfItems).count; + } + + private Set pickSubset(List sourceSet, Random random, List countDistribution) { + var count = pickCount(random, countDistribution); + List items = new ArrayList<>(sourceSet); + Collections.shuffle(items, random); + return new HashSet<>(items.subList(0, count)); + } + + private List joinAllCombinations(String[]... partArrays) { + int size = 1; + for (String[] partArray : partArrays) { + size *= partArray.length; + } + List out = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + StringBuilder item = new StringBuilder(); + int sizePerIncrement = 1; + for (String[] partArray : partArrays) { + item.append(' '); + item.append(partArray[(i / sizePerIncrement) % partArray.length]); + sizePerIncrement *= partArray.length; + } + item.delete(0, 1); + out.add(item.toString()); + } + return out; + } +} diff --git a/src/main/java/org/acme/employeescheduling/rest/EmployeeScheduleDemoResource.java b/src/main/java/org/acme/employeescheduling/rest/EmployeeScheduleDemoResource.java new file mode 100644 index 0000000..c036401 --- /dev/null +++ b/src/main/java/org/acme/employeescheduling/rest/EmployeeScheduleDemoResource.java @@ -0,0 +1,51 @@ +package org.acme.employeescheduling.rest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.acme.employeescheduling.domain.EmployeeSchedule; +import org.acme.employeescheduling.rest.DemoDataGenerator.DemoData; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +@Tag(name = "Demo data", description = "Timefold-provided demo employee schedule data.") +@Path("demo-data") +public class EmployeeScheduleDemoResource { + + private final DemoDataGenerator dataGenerator; + + @Inject + public EmployeeScheduleDemoResource(DemoDataGenerator dataGenerator) { + this.dataGenerator = dataGenerator; + } + + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "List of demo data represented as IDs.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = DemoData.class, type = SchemaType.ARRAY))) }) + @Operation(summary = "List demo data.") + @GET + public DemoData[] list() { + return DemoData.values(); + } + + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Unsolved demo schedule.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = EmployeeSchedule.class)))}) + @Operation(summary = "Find an unsolved demo schedule by ID.") + @GET + @Path("/{demoDataId}") + public Response generate(@PathParam("demoDataId") DemoData demoData) { + return Response.ok(dataGenerator.generateDemoData(demoData)).build(); + } +} diff --git a/src/main/java/org/acme/employeescheduling/rest/EmployeeScheduleResource.java b/src/main/java/org/acme/employeescheduling/rest/EmployeeScheduleResource.java new file mode 100644 index 0000000..2ab429f --- /dev/null +++ b/src/main/java/org/acme/employeescheduling/rest/EmployeeScheduleResource.java @@ -0,0 +1,201 @@ +package org.acme.employeescheduling.rest; + +import java.util.Collection; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; +import ai.timefold.solver.core.api.score.buildin.hardsoftbigdecimal.HardSoftBigDecimalScore; +import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; +import ai.timefold.solver.core.api.solver.SolutionManager; +import ai.timefold.solver.core.api.solver.SolverManager; +import ai.timefold.solver.core.api.solver.SolverStatus; + +import org.acme.employeescheduling.domain.EmployeeSchedule; +import org.acme.employeescheduling.rest.exception.EmployeeScheduleSolverException; +import org.acme.employeescheduling.rest.exception.ErrorInfo; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Tag(name = "Employee Schedules", description = "Employee Schedules service for assigning employees to shifts.") +@Path("schedules") +public class EmployeeScheduleResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeScheduleResource.class); + + SolverManager solverManager; + SolutionManager solutionManager; + + // TODO: Without any "time to live", the map may eventually grow out of memory. + private final ConcurrentMap jobIdToJob = new ConcurrentHashMap<>(); + + @Inject + public EmployeeScheduleResource(SolverManager solverManager, + SolutionManager solutionManager) { + this.solverManager = solverManager; + this.solutionManager = solutionManager; + } + + @Operation(summary = "List the job IDs of all submitted schedules.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "List of all job IDs.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = String.class))) }) + @GET + @Produces(MediaType.APPLICATION_JSON) + public Collection list() { + return jobIdToJob.keySet(); + } + + @Operation(summary = "Submit a schedule to start solving as soon as CPU resources are available.") + @APIResponses(value = { + @APIResponse(responseCode = "202", + description = "The job ID. Use that ID to get the solution with the other methods.", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) }) + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces(MediaType.TEXT_PLAIN) + public String solve(EmployeeSchedule problem) { + String jobId = UUID.randomUUID().toString(); + jobIdToJob.put(jobId, Job.ofSchedule(problem)); + solverManager.solveBuilder() + .withProblemId(jobId) + .withProblemFinder(jobId_ -> jobIdToJob.get(jobId).schedule) + .withBestSolutionConsumer(solution -> jobIdToJob.put(jobId, Job.ofSchedule(solution))) + .withExceptionHandler((jobId_, exception) -> { + jobIdToJob.put(jobId, Job.ofException(exception)); + LOGGER.error("Failed solving jobId ({}).", jobId, exception); + }) + .run(); + return jobId; + } + + @Operation(summary = "Submit a schedule to analyze its score.") + @APIResponses(value = { + @APIResponse(responseCode = "202", + description = "Resulting score analysis, optionally without constraint matches.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ScoreAnalysis.class))) }) + @PUT + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces(MediaType.APPLICATION_JSON) + @Path("analyze") + public ScoreAnalysis analyze(EmployeeSchedule problem, + @QueryParam("fetchPolicy") ScoreAnalysisFetchPolicy fetchPolicy) { + return fetchPolicy == null ? solutionManager.analyze(problem) : solutionManager.analyze(problem, fetchPolicy); + } + + @Operation( + summary = "Get the solution and score for a given job ID. This is the best solution so far, as it might still be running or not even started.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "The best solution of the schedule so far.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = EmployeeSchedule.class))), + @APIResponse(responseCode = "404", description = "No schedule found.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorInfo.class))), + @APIResponse(responseCode = "500", description = "Exception during solving a schedule.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorInfo.class))) + }) + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{jobId}") + public EmployeeSchedule getEmployeeSchedule( + @Parameter(description = "The job ID returned by the POST method.") @PathParam("jobId") String jobId) { + EmployeeSchedule schedule = getEmployeeScheduleAndCheckForExceptions(jobId); + SolverStatus solverStatus = solverManager.getSolverStatus(jobId); + schedule.setSolverStatus(solverStatus); + return schedule; + } + + private EmployeeSchedule getEmployeeScheduleAndCheckForExceptions(String jobId) { + Job job = jobIdToJob.get(jobId); + if (job == null) { + throw new EmployeeScheduleSolverException(jobId, Response.Status.NOT_FOUND, "No schedule found."); + } + if (job.exception != null) { + throw new EmployeeScheduleSolverException(jobId, job.exception); + } + return job.schedule; + } + + @Operation( + summary = "Terminate solving for a given job ID. Returns the best solution of the schedule so far, as it might still be running or not even started.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "The best solution of the schedule so far.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = EmployeeSchedule.class))), + @APIResponse(responseCode = "404", description = "No schedule found.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorInfo.class))), + @APIResponse(responseCode = "500", description = "Exception during solving a schedule.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorInfo.class))) + }) + @DELETE + @Produces(MediaType.APPLICATION_JSON) + @Path("{jobId}") + public EmployeeSchedule terminateSolving( + @Parameter(description = "The job ID returned by the POST method.") @PathParam("jobId") String jobId) { + // TODO: Replace with .terminateEarlyAndWait(... [, timeout]); see https://github.com/TimefoldAI/timefold-solver/issues/77 + solverManager.terminateEarly(jobId); + return getEmployeeSchedule(jobId); + } + + @Operation( + summary = "Get the schedule status and score for a given job ID.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "The schedule status and the best score so far.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = EmployeeSchedule.class))), + @APIResponse(responseCode = "404", description = "No schedule found.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorInfo.class))), + @APIResponse(responseCode = "500", description = "Exception during solving a schedule.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorInfo.class))) + }) + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{jobId}/status") + public EmployeeSchedule getStatus( + @Parameter(description = "The job ID returned by the POST method.") @PathParam("jobId") String jobId) { + EmployeeSchedule schedule = getEmployeeScheduleAndCheckForExceptions(jobId); + SolverStatus solverStatus = solverManager.getSolverStatus(jobId); + return new EmployeeSchedule(schedule.getScore(), solverStatus); + } + + private record Job(EmployeeSchedule schedule, Throwable exception) { + + static Job ofSchedule(EmployeeSchedule schedule) { + return new Job(schedule, null); + } + + static Job ofException(Throwable error) { + return new Job(null, error); + } + } +} diff --git a/src/main/java/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverException.java b/src/main/java/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverException.java new file mode 100644 index 0000000..b2fc316 --- /dev/null +++ b/src/main/java/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverException.java @@ -0,0 +1,30 @@ +package org.acme.employeescheduling.rest.exception; + +import jakarta.ws.rs.core.Response; + +public class EmployeeScheduleSolverException extends RuntimeException { + + private final String jobId; + + private final Response.Status status; + + public EmployeeScheduleSolverException(String jobId, Response.Status status, String message) { + super(message); + this.jobId = jobId; + this.status = status; + } + + public EmployeeScheduleSolverException(String jobId, Throwable cause) { + super(cause.getMessage(), cause); + this.jobId = jobId; + this.status = Response.Status.INTERNAL_SERVER_ERROR; + } + + public String getJobId() { + return jobId; + } + + public Response.Status getStatus() { + return status; + } +} diff --git a/src/main/java/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverExceptionMapper.java b/src/main/java/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverExceptionMapper.java new file mode 100644 index 0000000..481079e --- /dev/null +++ b/src/main/java/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverExceptionMapper.java @@ -0,0 +1,19 @@ +package org.acme.employeescheduling.rest.exception; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class EmployeeScheduleSolverExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(EmployeeScheduleSolverException exception) { + return Response + .status(exception.getStatus()) + .type(MediaType.APPLICATION_JSON) + .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) + .build(); + } +} diff --git a/src/main/java/org/acme/employeescheduling/rest/exception/ErrorInfo.java b/src/main/java/org/acme/employeescheduling/rest/exception/ErrorInfo.java new file mode 100644 index 0000000..5ab81f4 --- /dev/null +++ b/src/main/java/org/acme/employeescheduling/rest/exception/ErrorInfo.java @@ -0,0 +1,4 @@ +package org.acme.employeescheduling.rest.exception; + +public record ErrorInfo(String jobId, String message) { +} diff --git a/src/main/java/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProvider.java b/src/main/java/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProvider.java new file mode 100644 index 0000000..63a2071 --- /dev/null +++ b/src/main/java/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProvider.java @@ -0,0 +1,123 @@ +package org.acme.employeescheduling.solver; + +import static ai.timefold.solver.core.api.score.stream.Joiners.equal; +import static ai.timefold.solver.core.api.score.stream.Joiners.lessThanOrEqual; +import static ai.timefold.solver.core.api.score.stream.Joiners.overlapping; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.function.Function; + +import ai.timefold.solver.core.api.score.buildin.hardsoftbigdecimal.HardSoftBigDecimalScore; +import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; +import ai.timefold.solver.core.api.score.stream.ConstraintFactory; +import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.score.stream.common.LoadBalance; + +import org.acme.employeescheduling.domain.Employee; +import org.acme.employeescheduling.domain.Shift; + +public class EmployeeSchedulingConstraintProvider implements ConstraintProvider { + + private static int getMinuteOverlap(Shift shift1, Shift shift2) { + // The overlap of two timeslot occurs in the range common to both timeslots. + // Both timeslots are active after the higher of their two start times, + // and before the lower of their two end times. + LocalDateTime shift1Start = shift1.getStart(); + LocalDateTime shift1End = shift1.getEnd(); + LocalDateTime shift2Start = shift2.getStart(); + LocalDateTime shift2End = shift2.getEnd(); + return (int) Duration.between((shift1Start.isAfter(shift2Start)) ? shift1Start : shift2Start, + (shift1End.isBefore(shift2End)) ? shift1End : shift2End).toMinutes(); + } + + @Override + public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { + return new Constraint[] { + // Hard constraints + requiredSkill(constraintFactory), + noOverlappingShifts(constraintFactory), + atLeast10HoursBetweenTwoShifts(constraintFactory), + oneShiftPerDay(constraintFactory), + unavailableEmployee(constraintFactory), + // Soft constraints + undesiredDayForEmployee(constraintFactory), + desiredDayForEmployee(constraintFactory), + balanceEmployeeShiftAssignments(constraintFactory) + }; + } + + Constraint requiredSkill(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Shift.class) + .filter(shift -> !shift.getEmployee().getSkills().contains(shift.getRequiredSkill())) + .penalize(HardSoftBigDecimalScore.ONE_HARD) + .asConstraint("Missing required skill"); + } + + Constraint noOverlappingShifts(ConstraintFactory constraintFactory) { + return constraintFactory.forEachUniquePair(Shift.class, equal(Shift::getEmployee), + overlapping(Shift::getStart, Shift::getEnd)) + .penalize(HardSoftBigDecimalScore.ONE_HARD, + EmployeeSchedulingConstraintProvider::getMinuteOverlap) + .asConstraint("Overlapping shift"); + } + + Constraint atLeast10HoursBetweenTwoShifts(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Shift.class) + .join(Shift.class, equal(Shift::getEmployee), lessThanOrEqual(Shift::getEnd, Shift::getStart)) + .filter((firstShift, + secondShift) -> Duration.between(firstShift.getEnd(), secondShift.getStart()).toHours() < 10) + .penalize(HardSoftBigDecimalScore.ONE_HARD, + (firstShift, secondShift) -> { + int breakLength = (int) Duration.between(firstShift.getEnd(), secondShift.getStart()).toMinutes(); + return (10 * 60) - breakLength; + }) + .asConstraint("At least 10 hours between 2 shifts"); + } + + Constraint oneShiftPerDay(ConstraintFactory constraintFactory) { + return constraintFactory.forEachUniquePair(Shift.class, equal(Shift::getEmployee), + equal(shift -> shift.getStart().toLocalDate())) + .penalize(HardSoftBigDecimalScore.ONE_HARD) + .asConstraint("Max one shift per day"); + } + + Constraint unavailableEmployee(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Shift.class) + .join(Employee.class, equal(Shift::getEmployee, Function.identity())) + .flattenLast(Employee::getUnavailableDates) + .filter(Shift::isOverlappingWithDate) + .penalize(HardSoftBigDecimalScore.ONE_HARD, Shift::getOverlappingDurationInMinutes) + .asConstraint("Unavailable employee"); + } + + Constraint undesiredDayForEmployee(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Shift.class) + .join(Employee.class, equal(Shift::getEmployee, Function.identity())) + .flattenLast(Employee::getUndesiredDates) + .filter(Shift::isOverlappingWithDate) + .penalize(HardSoftBigDecimalScore.ONE_SOFT, Shift::getOverlappingDurationInMinutes) + .asConstraint("Undesired day for employee"); + } + + Constraint desiredDayForEmployee(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Shift.class) + .join(Employee.class, equal(Shift::getEmployee, Function.identity())) + .flattenLast(Employee::getDesiredDates) + .filter(Shift::isOverlappingWithDate) + .reward(HardSoftBigDecimalScore.ONE_SOFT, Shift::getOverlappingDurationInMinutes) + .asConstraint("Desired day for employee"); + } + + Constraint balanceEmployeeShiftAssignments(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Shift.class) + .groupBy(Shift::getEmployee, ConstraintCollectors.count()) + .complement(Employee.class, e -> 0) // Include all employees which are not assigned to any shift.c + .groupBy(ConstraintCollectors.loadBalance((employee, shiftCount) -> employee, + (employee, shiftCount) -> shiftCount)) + .penalizeBigDecimal(HardSoftBigDecimalScore.ONE_SOFT, LoadBalance::unfairness) + .asConstraint("Balance employee shift assignments"); + } + +} diff --git a/src/main/resources/META-INF/resources/app.js b/src/main/resources/META-INF/resources/app.js new file mode 100644 index 0000000..eca3308 --- /dev/null +++ b/src/main/resources/META-INF/resources/app.js @@ -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($('' + item + '')); + $("#" + 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("

No test data available

") + }); +} + +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 = $('
') + .append($(`
)`) + .append(employee.name)) + .append($('
') + .append($(employee.skills.map(skill => `${skill}`).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 = $(`
`) + .append($(`
`).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 = $(`
`) + .append($(`
`).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 = $(`
`) + .append($(`
`).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 = $('
') + .append($(`
)`) + .append("Unassigned")) + .append($('
') + .append($(`${shift.requiredSkill}`))); + + 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 = $('
') + .append($(`
)`) + .append(shift.location)) + .append($('
') + .append($(`${shift.requiredSkill}`))); + const byLocationShiftElement = $('
') + .append($(`
)`) + .append(shift.employee.name)) + .append($('
') + .append($(`${shift.requiredSkill}`))); + + 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($(`

`).text(`There are no unassigned shifts.`)); + } else { + unassignedShifts.append($(`

`).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 = $(``).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(/(-?\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( + $(`
+ +
`)); + } + + const timefoldFooter = $("footer#timefold-auto-footer"); + if (timefoldFooter != null) { + timefoldFooter.append( + $(``)); + } +} diff --git a/src/main/resources/META-INF/resources/index.html b/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000..7c1b361 --- /dev/null +++ b/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,161 @@ + + + + + Employee scheduling - Timefold Solver on Quarkus + + + + + + + + + + +
+ +
+
+
+
+
+
+

Employee scheduling solver

+

Generate the optimal schedule for your employees.

+ +
+ + + + Score: ? + + +
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+

REST API Guide

+ +

Employee Scheduling solver integration via cURL

+ +

1. Download demo data

+
+            
+            curl -X GET -H 'Accept:application/json' http://localhost:8080/demo-data/SMALL -o sample.json
+    
+ +

2. Post the sample data for solving

+

The POST operation returns a jobId that should be used in subsequent commands.

+
+            
+            curl -X POST -H 'Content-Type:application/json' http://localhost:8080/schedules -d@sample.json
+    
+ +

3. Get the current status and score

+
+            
+            curl -X GET -H 'Accept:application/json' http://localhost:8080/schedules/{jobId}/status
+    
+ +

4. Get the complete solution

+
+            
+            curl -X GET -H 'Accept:application/json' http://localhost:8080/schedules/{jobId}
+    
+ +

5. Fetch the analysis of the solution

+
+            
+            curl -X PUT -H 'Content-Type:application/json' http://localhost:8080/schedules/analyze -d@solution.json
+    
+ +

5. Terminate solving early

+
+            
+            curl -X DELETE -H 'Accept:application/json' http://localhost:8080/schedules/{id}
+    
+
+ +
+

REST API Reference

+
+ + +
+
+
+
+ + + + + + + + + + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..2e80d17 --- /dev/null +++ b/src/main/resources/application.properties @@ -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 diff --git a/target/build-metrics.json b/target/build-metrics.json new file mode 100644 index 0000000..db0681a --- /dev/null +++ b/target/build-metrics.json @@ -0,0 +1 @@ +{"duration":1238,"records":[{"duration":413,"stepId":"io.quarkus.deployment.index.ApplicationArchiveBuildStep#build","started":"16:23:20.294","dependents":[374,275,273,307,276,436,337,272,418],"id":271,"thread":"build-42"},{"duration":291,"stepId":"io.quarkus.smallrye.openapi.deployment.SmallRyeOpenApiProcessor#build","started":"16:23:20.832","dependents":[439],"id":403,"thread":"build-6"},{"duration":171,"stepId":"io.quarkus.deployment.console.ConsoleProcessor#setupConsole","started":"16:23:20.193","dependents":[269,273,268,267],"id":266,"thread":"build-2"},{"duration":155,"stepId":"io.quarkus.deployment.steps.ConfigGenerationBuildStep#generateConfigClass","started":"16:23:20.193","dependents":[],"id":265,"thread":"build-38"},{"duration":152,"stepId":"io.quarkus.swaggerui.deployment.SwaggerUiProcessor#getSwaggerUiFinalDestination","started":"16:23:20.257","dependents":[405],"id":270,"thread":"build-32"},{"duration":140,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#createDevUILog","started":"16:23:20.197","dependents":[439,263,433],"id":258,"thread":"build-50"},{"duration":125,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#bodyHandler","started":"16:23:20.202","dependents":[439,433],"id":256,"thread":"build-27"},{"duration":111,"stepId":"io.quarkus.deployment.steps.BannerProcessor#recordBanner","started":"16:23:20.199","dependents":[439,332],"id":253,"thread":"build-51"},{"duration":107,"stepId":"io.quarkus.deployment.steps.MainClassBuildStep#build","started":"16:23:21.289","dependents":[],"id":439,"thread":"build-6"},{"duration":105,"stepId":"io.quarkus.arc.deployment.ArcProcessor#generateResources","started":"16:23:20.975","dependents":[389,438,418],"id":388,"thread":"build-2"},{"duration":99,"stepId":"io.quarkus.deployment.steps.ConfigDescriptionBuildStep#createConfigDescriptions","started":"16:23:20.193","dependents":[260,262,254],"id":244,"thread":"build-42"},{"duration":98,"stepId":"io.quarkus.resteasy.reactive.common.deployment.ResteasyReactiveCommonProcessor#setupBlockingOperationSupport","started":"16:23:20.165","dependents":[439],"id":221,"thread":"build-3"},{"duration":92,"stepId":"io.quarkus.arc.deployment.ArcProcessor#registerBeans","started":"16:23:20.839","dependents":[365,354,356,359,355,368,360,357,370,362,366,363,361,415,358,367],"id":353,"thread":"build-62"},{"duration":91,"stepId":"io.quarkus.devui.deployment.menu.DependenciesProcessor#createAppDeps","started":"16:23:20.213","dependents":[404],"id":250,"thread":"build-39"},{"duration":91,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#dontPropagateCdiContext","started":"16:23:20.193","dependents":[439,242],"id":240,"thread":"build-11"},{"duration":89,"stepId":"io.quarkus.devui.deployment.DevUIProcessor#getAllExtensions","started":"16:23:21.018","dependents":[402,401,404,405,406],"id":400,"thread":"build-90"},{"duration":88,"stepId":"io.quarkus.deployment.steps.ApplicationIndexBuildStep#build","started":"16:23:20.205","dependents":[271,252,360,357,352,408],"id":245,"thread":"build-60"},{"duration":88,"stepId":"io.quarkus.webdependency.locator.deployment.WebDependencyLocatorProcessor#findWebDependenciesAndCreateHandler","started":"16:23:20.206","dependents":[249,439,430,431],"id":247,"thread":"build-62"},{"duration":86,"stepId":"io.quarkus.devui.deployment.BuildTimeContentProcessor#createBuildTimeConstJsTemplate","started":"16:23:21.128","dependents":[428,427],"id":426,"thread":"build-6"},{"duration":85,"stepId":"io.quarkus.jackson.deployment.JacksonProcessor#generateCustomizer","started":"16:23:20.719","dependents":[337],"id":336,"thread":"build-62"},{"duration":84,"stepId":"io.quarkus.deployment.logging.LoggingResourceProcessor#logConsoleCommand","started":"16:23:20.166","dependents":[412],"id":202,"thread":"build-8"},{"duration":84,"stepId":"io.quarkus.deployment.dev.io.NioThreadPoolDevModeProcessor#setupTCCL","started":"16:23:20.184","dependents":[439],"id":226,"thread":"build-33"},{"duration":82,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#createVertxThreadFactory","started":"16:23:20.193","dependents":[243,439],"id":236,"thread":"build-41"},{"duration":78,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#fileHandling","started":"16:23:20.189","dependents":[417,415],"id":224,"thread":"build-40"},{"duration":74,"stepId":"io.quarkus.smallrye.context.deployment.SmallRyeContextPropagationProcessor#buildStatic","started":"16:23:20.195","dependents":[439],"id":229,"thread":"build-21"},{"duration":74,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#configureLogging","started":"16:23:20.195","dependents":[439],"id":231,"thread":"build-34"},{"duration":73,"stepId":"io.quarkus.devui.deployment.menu.DevServicesProcessor#createDevServicesPages","started":"16:23:20.196","dependents":[404,264],"id":230,"thread":"build-14"},{"duration":72,"stepId":"io.quarkus.vertx.http.deployment.console.ConsoleProcessor#setupConsole","started":"16:23:20.196","dependents":[434],"id":227,"thread":"build-47"},{"duration":72,"stepId":"io.quarkus.deployment.steps.ReflectiveHierarchyStep#ignoreJavaClassWarnings","started":"16:23:20.190","dependents":[416],"id":217,"thread":"build-35"},{"duration":71,"stepId":"io.quarkus.mutiny.deployment.MutinyProcessor#buildTimeInit","started":"16:23:20.176","dependents":[439],"id":196,"thread":"build-16"},{"duration":71,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#resetMapper","started":"16:23:20.198","dependents":[439],"id":232,"thread":"build-44"},{"duration":70,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#registerFormAuthMechanism","started":"16:23:20.193","dependents":[439,368,366,367],"id":220,"thread":"build-17"},{"duration":70,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveCDIProcessor#beanDefiningAnnotations","started":"16:23:20.174","dependents":[352,337,228],"id":190,"thread":"build-9"},{"duration":69,"stepId":"io.quarkus.devui.deployment.build.BuildMetricsDevUIProcessor#create","started":"16:23:20.186","dependents":[439],"id":206,"thread":"build-32"},{"duration":69,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#ioThreadDetector","started":"16:23:20.201","dependents":[439,239],"id":233,"thread":"build-53"},{"duration":65,"stepId":"io.quarkus.devui.deployment.menu.ConfigurationProcessor#registerBuildTimeActions","started":"16:23:20.169","dependents":[439,368,366,264,367],"id":186,"thread":"build-7"},{"duration":65,"stepId":"io.quarkus.deployment.steps.DevServicesConfigBuildStep#setup","started":"16:23:20.190","dependents":[260,384,262,254,270,434],"id":207,"thread":"build-22"},{"duration":65,"stepId":"ai.timefold.solver.quarkus.deployment.TimefoldProcessor#recordAndRegisterBuildTimeBeans","started":"16:23:20.720","dependents":[416,368,366,334,352,337,373,335,367,379,418],"id":333,"thread":"build-20"},{"duration":64,"stepId":"io.quarkus.devui.deployment.menu.BuildMetricsProcessor#create","started":"16:23:20.166","dependents":[439],"id":182,"thread":"build-12"},{"duration":63,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#eventLoopCount","started":"16:23:20.195","dependents":[437,439],"id":212,"thread":"build-37"},{"duration":63,"stepId":"io.quarkus.deployment.steps.ConfigGenerationBuildStep#releaseConfigOnShutdown","started":"16:23:20.170","dependents":[439],"id":183,"thread":"build-15"},{"duration":60,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#initBasicAuth","started":"16:23:20.202","dependents":[439,351,368,366,349,367],"id":218,"thread":"build-54"},{"duration":56,"stepId":"io.quarkus.resteasy.reactive.jackson.deployment.processor.ResteasyReactiveJacksonProcessor#jsonDefault","started":"16:23:20.188","dependents":[408],"id":191,"thread":"build-28"},{"duration":56,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#cors","started":"16:23:20.207","dependents":[439,433,424],"id":222,"thread":"build-66"},{"duration":56,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#setupEndpoints","started":"16:23:21.087","dependents":[439,416,409,415,438,418,411,419],"id":408,"thread":"build-82"},{"duration":56,"stepId":"io.quarkus.netty.deployment.NettyProcessor#build","started":"16:23:20.204","dependents":[234,438],"id":214,"thread":"build-4"},{"duration":56,"stepId":"io.quarkus.virtual.threads.deployment.VirtualThreadsProcessor#setup","started":"16:23:20.207","dependents":[439,368,366,352,337,367],"id":219,"thread":"build-65"},{"duration":56,"stepId":"io.quarkus.netty.deployment.NettyProcessor#eagerlyInitClass","started":"16:23:20.190","dependents":[439],"id":194,"thread":"build-24"},{"duration":55,"stepId":"io.quarkus.deployment.steps.ConfigGenerationBuildStep#reportDeprecatedMappingProperties","started":"16:23:20.202","dependents":[439],"id":210,"thread":"build-36"},{"duration":55,"stepId":"io.quarkus.vertx.deployment.VertxProcessor#currentContextFactory","started":"16:23:20.193","dependents":[439,389],"id":199,"thread":"build-6"},{"duration":54,"stepId":"io.quarkus.devui.deployment.menu.WorkspaceProcessor#locateWorkspaceItems","started":"16:23:20.194","dependents":[237],"id":200,"thread":"build-5"},{"duration":52,"stepId":"io.quarkus.webdependency.locator.deployment.devui.WebDependencyLocatorDevModeApiProcessor#findWebDependenciesAssets","started":"16:23:20.209","dependents":[249],"id":215,"thread":"build-30"},{"duration":51,"stepId":"io.quarkus.vertx.http.deployment.GeneratedStaticResourcesProcessor#process","started":"16:23:20.197","dependents":[439,430,431,436],"id":197,"thread":"build-49"},{"duration":51,"stepId":"io.quarkus.devui.deployment.menu.MCPProcessor#registerDevUiHandlers","started":"16:23:20.195","dependents":[439,430,431],"id":195,"thread":"build-46"},{"duration":50,"stepId":"io.quarkus.devui.deployment.menu.ExtensionsProcessor#createBuildTimeActions","started":"16:23:20.199","dependents":[264],"id":198,"thread":"build-52"},{"duration":49,"stepId":"ai.timefold.solver.quarkus.deployment.TimefoldProcessor#registerRPCService","started":"16:23:20.204","dependents":[235,300,241],"id":203,"thread":"build-18"},{"duration":49,"stepId":"io.quarkus.deployment.logging.LoggingResourceProcessor#setupLoggingStaticInit","started":"16:23:20.205","dependents":[439],"id":204,"thread":"build-56"},{"duration":48,"stepId":"io.quarkus.deployment.steps.DevModeBuildStep#watchChanges","started":"16:23:20.206","dependents":[238],"id":205,"thread":"build-63"},{"duration":48,"stepId":"io.quarkus.devui.deployment.ide.IdeProcessor#createOpenInIDEService","started":"16:23:20.197","dependents":[439,430,431,264],"id":193,"thread":"build-48"},{"duration":47,"stepId":"io.quarkus.vertx.http.deployment.console.ConsoleProcessor#config","started":"16:23:20.307","dependents":[412],"id":262,"thread":"build-8"},{"duration":45,"stepId":"io.quarkus.deployment.steps.ClassPathSystemPropBuildStep#set","started":"16:23:20.212","dependents":[439],"id":209,"thread":"build-59"},{"duration":45,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#createHttpAuthenticationHandler","started":"16:23:20.222","dependents":[439,225,396],"id":223,"thread":"build-31"},{"duration":44,"stepId":"io.quarkus.deployment.steps.ConfigGenerationBuildStep#checkForBuildTimeConfigChange","started":"16:23:20.206","dependents":[439],"id":201,"thread":"build-61"},{"duration":44,"stepId":"io.quarkus.devui.deployment.menu.ConfigurationProcessor#registerConfigs","started":"16:23:20.294","dependents":[439],"id":260,"thread":"build-61"},{"duration":44,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#build_ab8f5337a8a378f6d88c5272f38f49e13900f727","started":"16:23:20.293","dependents":[437,439,393,368,431,366,364,259,433,434,261,367],"id":257,"thread":"build-6"},{"duration":41,"stepId":"io.quarkus.deployment.steps.PreloadClassesBuildStep#preInit","started":"16:23:20.186","dependents":[439],"id":181,"thread":"build-23"},{"duration":40,"stepId":"io.quarkus.deployment.logging.LoggingResourceProcessor#setupLoggingRuntimeInit","started":"16:23:20.721","dependents":[435,439,438],"id":332,"thread":"build-44"},{"duration":39,"stepId":"io.quarkus.vertx.http.deployment.GeneratedStaticResourcesProcessor#produceResources","started":"16:23:20.197","dependents":[216],"id":188,"thread":"build-19"},{"duration":38,"stepId":"io.quarkus.deployment.pkg.steps.JarResultBuildStep#outputTarget","started":"16:23:20.195","dependents":[273,189,187,403],"id":184,"thread":"build-45"},{"duration":37,"stepId":"io.quarkus.resteasy.reactive.common.deployment.ResteasyReactiveCommonProcessor#checkMixingStacks","started":"16:23:20.222","dependents":[434],"id":213,"thread":"build-20"},{"duration":37,"stepId":"io.quarkus.devui.deployment.DevUIProcessor#registerDevUiHandlers","started":"16:23:21.243","dependents":[439,430,431],"id":429,"thread":"build-2"},{"duration":36,"stepId":"io.quarkus.deployment.SslProcessor#setupNativeSsl","started":"16:23:20.197","dependents":[234],"id":185,"thread":"build-29"},{"duration":34,"stepId":"io.quarkus.vertx.http.deployment.ManagementInterfaceSecurityProcessor#createManagementAuthMechHandler","started":"16:23:20.222","dependents":[439,395,211],"id":208,"thread":"build-10"},{"duration":31,"stepId":"io.quarkus.devui.deployment.DevUIProcessor#additionalBean","started":"16:23:20.254","dependents":[275,352,337],"id":241,"thread":"build-61"},{"duration":29,"stepId":"io.quarkus.deployment.steps.ConfigGenerationBuildStep#generateMappings","started":"16:23:20.723","dependents":[356,376,358,438,379],"id":330,"thread":"build-60"},{"duration":29,"stepId":"io.quarkus.smallrye.context.deployment.SmallRyeContextPropagationProcessor#build","started":"16:23:20.294","dependents":[439,368,366,367],"id":255,"thread":"build-44"},{"duration":29,"stepId":"io.quarkus.deployment.steps.RuntimeConfigSetupBuildStep#setupRuntimeConfig","started":"16:23:20.161","dependents":[369,231,257,375,434,349,253,243,240,437,232,242,261,218,422,365,197,407,220,431,212,334,433,396,421,332,432,435,439,393,222,258,236,424,335,395,256,398],"id":126,"thread":"build-2"},{"duration":28,"stepId":"io.quarkus.devui.deployment.BuildTimeContentProcessor#createIndexHtmlTemplate","started":"16:23:21.214","dependents":[428],"id":427,"thread":"build-2"},{"duration":26,"stepId":"io.quarkus.arc.deployment.ArcProcessor#buildCompatibleExtensions","started":"16:23:20.166","dependents":[352,337],"id":132,"thread":"build-6"},{"duration":23,"stepId":"io.quarkus.arc.deployment.ArcProcessor#validate","started":"16:23:20.949","dependents":[374,385,388,375,377,381,376,378,379,418],"id":373,"thread":"build-60"},{"duration":23,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#addRoutingCtxToSecurityEventsForCdiBeans","started":"16:23:20.222","dependents":[439],"id":192,"thread":"build-25"},{"duration":22,"stepId":"io.quarkus.devui.deployment.menu.WorkspaceProcessor#createBuildTimeActions","started":"16:23:20.254","dependents":[264],"id":237,"thread":"build-6"},{"duration":22,"stepId":"io.quarkus.vertx.http.deployment.webjar.WebJarProcessor#processWebJarDevMode","started":"16:23:21.109","dependents":[439,407,406],"id":405,"thread":"build-57"},{"duration":21,"stepId":"io.quarkus.arc.deployment.devui.JsonRpcMethodsProcessor#jsonRpcMethods","started":"16:23:20.254","dependents":[342],"id":235,"thread":"build-8"},{"duration":21,"stepId":"io.quarkus.deployment.dev.HotDeploymentWatchedFileBuildStep#setupWatchedFileHotDeployment","started":"16:23:20.255","dependents":[434],"id":238,"thread":"build-56"},{"duration":21,"stepId":"io.quarkus.deployment.console.ConsoleProcessor#helpCommand","started":"16:23:20.168","dependents":[412],"id":122,"thread":"build-10"},{"duration":20,"stepId":"io.quarkus.vertx.deployment.VertxProcessor#autoAddScope","started":"16:23:20.186","dependents":[343],"id":163,"thread":"build-25"},{"duration":19,"stepId":"io.quarkus.vertx.http.deployment.StaticResourcesProcessor#collectStaticResources","started":"16:23:20.243","dependents":[393],"id":216,"thread":"build-12"},{"duration":19,"stepId":"io.quarkus.arc.deployment.LoggingBeanSupportProcessor#discoveredComponents","started":"16:23:20.175","dependents":[352,337,228],"id":145,"thread":"build-20"},{"duration":18,"stepId":"io.quarkus.devui.deployment.BuildTimeContentProcessor#createBuildTimeData","started":"16:23:21.109","dependents":[426,427],"id":404,"thread":"build-90"},{"duration":18,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor#handleCustomAnnotatedMethods","started":"16:23:20.730","dependents":[331,352,337,329],"id":328,"thread":"build-56"},{"duration":18,"stepId":"io.quarkus.deployment.recording.substitutions.AdditionalSubstitutionsBuildStep#additionalSubstitutions","started":"16:23:20.190","dependents":[439],"id":166,"thread":"build-39"},{"duration":18,"stepId":"io.quarkus.devui.deployment.menu.ConfigurationProcessor#createConfigurationPages","started":"16:23:20.312","dependents":[404],"id":254,"thread":"build-20"},{"duration":18,"stepId":"io.quarkus.arc.deployment.devui.ArcDevModeApiProcessor#collectBeanInfo","started":"16:23:20.973","dependents":[386],"id":385,"thread":"build-40"},{"duration":18,"stepId":"io.quarkus.arc.deployment.ShutdownBuildSteps#addScope","started":"16:23:20.189","dependents":[343],"id":167,"thread":"build-30"},{"duration":17,"stepId":"io.quarkus.arc.deployment.BeanArchiveProcessor#build","started":"16:23:20.805","dependents":[346,399,339,352,345,362,341,338,363,347,415,340,408,343],"id":337,"thread":"build-6"},{"duration":17,"stepId":"io.quarkus.arc.deployment.StartupBuildSteps#addScope","started":"16:23:20.190","dependents":[343],"id":164,"thread":"build-31"},{"duration":16,"stepId":"io.quarkus.deployment.steps.ConfigGenerationBuildStep#buildTimeRunTimeConfig","started":"16:23:20.195","dependents":[384,438],"id":171,"thread":"build-10"},{"duration":16,"stepId":"io.quarkus.deployment.ide.IdeProcessor#detectRunningIdeProcesses","started":"16:23:20.175","dependents":[136],"id":131,"thread":"build-19"},{"duration":16,"stepId":"io.quarkus.deployment.logging.LoggingResourceProcessor#registerMetrics","started":"16:23:20.197","dependents":[439,332],"id":172,"thread":"build-20"},{"duration":16,"stepId":"io.quarkus.deployment.console.ConsoleProcessor#quitCommand","started":"16:23:20.173","dependents":[412],"id":120,"thread":"build-5"},{"duration":16,"stepId":"io.quarkus.arc.deployment.devui.ArcDevUIProcessor#registerMonitoringComponents","started":"16:23:20.252","dependents":[352,337],"id":228,"thread":"build-15"},{"duration":15,"stepId":"io.quarkus.deployment.steps.CapabilityAggregationStep#provideCapabilities","started":"16:23:20.205","dependents":[175],"id":174,"thread":"build-58"},{"duration":14,"stepId":"io.quarkus.devui.deployment.menu.WorkspaceProcessor#createDefaultWorkspaceActions","started":"16:23:20.184","dependents":[237],"id":155,"thread":"build-18"},{"duration":14,"stepId":"io.quarkus.deployment.recording.AnnotationProxyBuildStep#build","started":"16:23:20.307","dependents":[364],"id":252,"thread":"build-53"},{"duration":14,"stepId":"io.quarkus.deployment.steps.CompiledJavaVersionBuildStep#compiledJavaVersion","started":"16:23:20.204","dependents":[408],"id":173,"thread":"build-57"},{"duration":14,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#setupDeployment","started":"16:23:21.158","dependents":[422,439,430,431,423,433,424,421,438,420],"id":419,"thread":"build-2"},{"duration":13,"stepId":"io.quarkus.deployment.steps.ClassTransformingBuildStep#handleClassTransformation","started":"16:23:21.147","dependents":[],"id":418,"thread":"build-83"},{"duration":13,"stepId":"io.quarkus.arc.deployment.ArcProcessor#setupExecutor","started":"16:23:20.302","dependents":[439],"id":251,"thread":"build-56"},{"duration":13,"stepId":"io.quarkus.arc.deployment.ArcProcessor#initialize","started":"16:23:20.825","dependents":[385,363,353],"id":352,"thread":"build-20"},{"duration":12,"stepId":"io.quarkus.deployment.steps.ApplicationInfoBuildStep#create","started":"16:23:20.195","dependents":[439],"id":162,"thread":"build-43"},{"duration":12,"stepId":"io.quarkus.mutiny.deployment.MutinyDevUIProcessor#createCard","started":"16:23:20.186","dependents":[400,387],"id":156,"thread":"build-36"},{"duration":11,"stepId":"io.quarkus.resteasy.reactive.jackson.deployment.processor.ResteasyReactiveJacksonProcessor#resolveRolesAllowedConfigExpressions","started":"16:23:20.719","dependents":[439,368,375,366,367],"id":313,"thread":"build-50"},{"duration":11,"stepId":"io.quarkus.arc.deployment.SplitPackageProcessor#splitPackageDetection","started":"16:23:20.708","dependents":[388],"id":276,"thread":"build-8"},{"duration":11,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#serverSerializers","started":"16:23:21.146","dependents":[439,438,419],"id":417,"thread":"build-82"},{"duration":11,"stepId":"io.quarkus.deployment.ExtensionLoader#config","started":"16:23:20.180","dependents":[134,269,231,141,266,136,240,245,157,426,376,171,427,271,139,220,225,334,135,265,152,276,236,160,270,138,395,199,386,244,369,306,308,381,345,434,228,419,243,437,140,154,143,330,144,365,197,223,200,385,146,162,212,396,352,184,432,148,150,439,307,230,151,158,258,172,320,337,344,408,403,153,274,208,402,351,257,360,389,357,349,185,253,232,404,242,261,234,379,215,297,198,431,433,332,211,233,418,159,429,424,335,256,210,161,273,384,247,350,218,203,422,214,407,204,388,333,165,201,205,303,189,219,421,192,435,393,222,168,405,342,398],"id":130,"thread":"build-26"},{"duration":11,"stepId":"io.quarkus.arc.deployment.devui.ArcDevUIProcessor#pages","started":"16:23:20.995","dependents":[400,387],"id":386,"thread":"build-68"},{"duration":10,"stepId":"io.quarkus.vertx.deployment.VertxJsonProcessor#registerJacksonSerDeser","started":"16:23:20.169","dependents":[336],"id":54,"thread":"build-14"},{"duration":10,"stepId":"io.quarkus.deployment.steps.CombinedIndexBuildStep#build","started":"16:23:20.708","dependents":[283,336,300,286,314,311,289,288,410,290,315,305,358,323,297,332,321,277,301,293,313,278,331,306,356,308,294,339,285,291,292,317,384,287,330,279,333,319,281,352,303,295,284,307,416,282,347,280,328,342],"id":275,"thread":"build-32"},{"duration":10,"stepId":"io.quarkus.deployment.steps.ReflectiveHierarchyStep#build","started":"16:23:21.144","dependents":[438],"id":416,"thread":"build-90"},{"duration":9,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#overrideContextInternalInterfaceToAddSafeGuards","started":"16:23:20.187","dependents":[418],"id":149,"thread":"build-29"},{"duration":9,"stepId":"io.quarkus.deployment.steps.NativeImageConfigBuildStep#build","started":"16:23:20.261","dependents":[439],"id":234,"thread":"build-20"},{"duration":8,"stepId":"io.quarkus.devui.deployment.DevUIProcessor#findAllJsonRPCMethods","started":"16:23:20.718","dependents":[426,397],"id":300,"thread":"build-6"},{"duration":8,"stepId":"io.quarkus.arc.deployment.SyntheticBeansProcessor#initRuntime","started":"16:23:20.939","dependents":[369,439,370,434],"id":368,"thread":"build-60"},{"duration":8,"stepId":"ai.timefold.solver.quarkus.deployment.TimefoldProcessor#indexDependencyBuildItem","started":"16:23:20.193","dependents":[271],"id":160,"thread":"build-4"},{"duration":7,"stepId":"io.quarkus.deployment.steps.MainClassBuildStep#mainClassBuildStep","started":"16:23:20.722","dependents":[418],"id":307,"thread":"build-15"},{"duration":7,"stepId":"ai.timefold.solver.quarkus.deployment.TimefoldProcessor#watchSolverConfigXml","started":"16:23:20.192","dependents":[238],"id":157,"thread":"build-27"},{"duration":7,"stepId":"io.quarkus.arc.deployment.ArcProcessor#quarkusMain","started":"16:23:20.165","dependents":[352,337,228],"id":34,"thread":"build-9"},{"duration":7,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#registerAuthMechanismSelectionInterceptor","started":"16:23:20.721","dependents":[439,360,320,305],"id":303,"thread":"build-34"},{"duration":6,"stepId":"io.quarkus.arc.deployment.ArcProcessor#loggerProducer","started":"16:23:20.177","dependents":[352,337],"id":79,"thread":"build-4"},{"duration":6,"stepId":"io.quarkus.deployment.logging.LoggingResourceProcessor#setupStackTraceFormatter","started":"16:23:20.708","dependents":[274,433,332],"id":273,"thread":"build-38"},{"duration":6,"stepId":"io.quarkus.deployment.steps.BlockingOperationControlBuildStep#blockingOP","started":"16:23:20.270","dependents":[439],"id":239,"thread":"build-44"},{"duration":6,"stepId":"io.quarkus.deployment.dev.testing.TestTracingProcessor#testConsoleCommand","started":"16:23:20.721","dependents":[412],"id":301,"thread":"build-27"},{"duration":6,"stepId":"io.quarkus.arc.deployment.ConfigStaticInitBuildSteps#registerBeans","started":"16:23:20.186","dependents":[352,337],"id":133,"thread":"build-21"},{"duration":6,"stepId":"io.quarkus.smallrye.openapi.deployment.SmallRyeOpenApiProcessor#addAutoFilters","started":"16:23:20.826","dependents":[403],"id":351,"thread":"build-62"},{"duration":6,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#responseHeaderSupport","started":"16:23:20.179","dependents":[408],"id":97,"thread":"build-28"},{"duration":6,"stepId":"io.quarkus.deployment.steps.ConfigGenerationBuildStep#generateBuilders","started":"16:23:20.975","dependents":[438],"id":384,"thread":"build-56"},{"duration":6,"stepId":"io.quarkus.smallrye.openapi.deployment.SmallRyeOpenApiProcessor#additionalBean","started":"16:23:20.177","dependents":[352,337],"id":78,"thread":"build-17"},{"duration":6,"stepId":"io.quarkus.credentials.deployment.CredentialsProcessor#unremoveable","started":"16:23:20.177","dependents":[373,379],"id":77,"thread":"build-21"},{"duration":5,"stepId":"io.quarkus.smallrye.openapi.deployment.SmallRyeOpenApiProcessor#registerAnnotatedUserDefinedRuntimeFilters","started":"16:23:20.826","dependents":[439,368,366,367,438],"id":350,"thread":"build-6"},{"duration":5,"stepId":"io.quarkus.arc.deployment.HotDeploymentConfigBuildStep#startup","started":"16:23:20.179","dependents":[95],"id":92,"thread":"build-27"},{"duration":5,"stepId":"io.quarkus.vertx.http.deployment.GeneratedStaticResourcesProcessor#devMode","started":"16:23:20.186","dependents":[238,197,188],"id":128,"thread":"build-34"},{"duration":5,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#securityContextOverrideHandler","started":"16:23:20.190","dependents":[419],"id":147,"thread":"build-14"},{"duration":5,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#shouldNotRemoveHttpServerOptionsCustomizers","started":"16:23:20.179","dependents":[373,379],"id":87,"thread":"build-23"},{"duration":5,"stepId":"io.quarkus.devui.deployment.BuildTimeContentProcessor#gatherMvnpmJars","started":"16:23:20.203","dependents":[429,427],"id":169,"thread":"build-55"},{"duration":5,"stepId":"io.quarkus.deployment.logging.LoggingResourceProcessor#setUpDefaultLogCleanupFilters","started":"16:23:20.179","dependents":[384],"id":93,"thread":"build-24"},{"duration":5,"stepId":"io.quarkus.deployment.steps.ConfigGenerationBuildStep#runtimeOverrideConfig","started":"16:23:20.179","dependents":[384],"id":91,"thread":"build-25"},{"duration":5,"stepId":"io.quarkus.resteasy.reactive.server.deployment.devui.ResteasyReactiveDevUIProcessor#createJsonRPCService","started":"16:23:20.188","dependents":[235,300,241],"id":142,"thread":"build-37"},{"duration":4,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveCDIProcessor#contextInjection","started":"16:23:20.187","dependents":[352,337,373,344,343,379],"id":129,"thread":"build-4"},{"duration":4,"stepId":"io.quarkus.resteasy.reactive.common.deployment.ResteasyReactiveCommonProcessor#scanResources","started":"16:23:20.723","dependents":[326,309,346,399,352,318,304,419,325,410,328,327,408,312,322,310],"id":302,"thread":"build-66"},{"duration":4,"stepId":"io.quarkus.resteasy.reactive.server.deployment.devui.ResteasyReactiveDevUIProcessor#createPages","started":"16:23:20.178","dependents":[400,387],"id":71,"thread":"build-11"},{"duration":4,"stepId":"io.quarkus.netty.deployment.NettyProcessor#setNettyMachineId","started":"16:23:20.187","dependents":[439],"id":127,"thread":"build-13"},{"duration":4,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor#asyncSupport","started":"16:23:20.179","dependents":[408],"id":82,"thread":"build-13"},{"duration":4,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#responseStatusSupport","started":"16:23:20.177","dependents":[408],"id":67,"thread":"build-22"},{"duration":4,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#filterMultipleVertxInstancesWarning","started":"16:23:20.173","dependents":[93,332],"id":49,"thread":"build-13"},{"duration":4,"stepId":"io.quarkus.devui.deployment.DevUIProcessor#createJsonRpcRouter","started":"16:23:21.086","dependents":[439],"id":397,"thread":"build-7"},{"duration":4,"stepId":"io.quarkus.deployment.steps.ThreadPoolSetup#createExecutor","started":"16:23:20.289","dependents":[255,248,439,251,257,433,246],"id":243,"thread":"build-61"},{"duration":4,"stepId":"io.quarkus.deployment.steps.ConfigGenerationBuildStep#unknownConfigFiles","started":"16:23:20.708","dependents":[439],"id":272,"thread":"build-6"},{"duration":4,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor#scanForExceptionMappers","started":"16:23:20.748","dependents":[352,337,438,419],"id":331,"thread":"build-6"},{"duration":4,"stepId":"io.quarkus.arc.deployment.CommandLineArgumentsProcessor#commandLineArgs","started":"16:23:20.189","dependents":[368,366,352,337,367],"id":137,"thread":"build-38"},{"duration":4,"stepId":"io.quarkus.vertx.http.deployment.devmode.NotFoundProcessor#routeNotFound","started":"16:23:21.283","dependents":[439],"id":436,"thread":"build-6"},{"duration":4,"stepId":"io.quarkus.deployment.logging.LoggingResourceProcessor#setUpDefaultLevels","started":"16:23:20.175","dependents":[384,332],"id":53,"thread":"build-18"},{"duration":3,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#preinitializeRouter","started":"16:23:20.338","dependents":[439,368,431,366,367],"id":261,"thread":"build-44"},{"duration":3,"stepId":"io.quarkus.resteasy.reactive.common.deployment.ResteasyReactiveCommonProcessor#setupEndpoints","started":"16:23:21.089","dependents":[417,415,414,438],"id":399,"thread":"build-83"},{"duration":3,"stepId":"io.quarkus.resteasy.reactive.common.deployment.ResteasyReactiveCommonProcessor#scanForParameterContainers","started":"16:23:20.729","dependents":[325],"id":323,"thread":"build-42"},{"duration":3,"stepId":"io.quarkus.deployment.dev.testing.TestTracingProcessor#startTesting","started":"16:23:20.364","dependents":[434,332],"id":269,"thread":"build-38"},{"duration":3,"stepId":"io.quarkus.resteasy.reactive.common.deployment.ResteasyReactiveCommonProcessor#handleApplication","started":"16:23:20.725","dependents":[331,323,319,314,399,329,311,419,321,317,417,315,408,438],"id":306,"thread":"build-40"},{"duration":3,"stepId":"io.quarkus.arc.deployment.staticmethods.InterceptedStaticMethodsProcessor#collectInterceptedStaticMethods","started":"16:23:20.932","dependents":[394,363,373,379],"id":362,"thread":"build-40"},{"duration":3,"stepId":"io.quarkus.smallrye.openapi.deployment.SmallRyeOpenApiProcessor#smallryeOpenApiIndex","started":"16:23:20.823","dependents":[351,350,348,403,349],"id":347,"thread":"build-56"},{"duration":3,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#finalizeRouter","started":"16:23:21.283","dependents":[435,439,434],"id":433,"thread":"build-2"},{"duration":3,"stepId":"io.quarkus.webdependency.locator.deployment.devui.WebDependencyLocatorDevUIProcessor#createPages","started":"16:23:20.295","dependents":[400,387],"id":249,"thread":"build-60"},{"duration":3,"stepId":"io.quarkus.jackson.deployment.JacksonProcessor#jacksonSupport","started":"16:23:20.721","dependents":[439,368,366,367],"id":297,"thread":"build-2"},{"duration":3,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#additionalReflection","started":"16:23:21.146","dependents":[438],"id":415,"thread":"build-57"},{"duration":2,"stepId":"io.quarkus.devui.deployment.DevUIProcessor#processFooterLogs","started":"16:23:20.342","dependents":[400,264,387],"id":263,"thread":"build-6"},{"duration":2,"stepId":"ai.timefold.solver.quarkus.deployment.TimefoldProcessor#recordAndRegisterDevUIBean","started":"16:23:20.785","dependents":[439,368,366,367],"id":334,"thread":"build-44"},{"duration":2,"stepId":"io.quarkus.netty.deployment.NettyProcessor#registerQualifiers","started":"16:23:20.186","dependents":[352,337],"id":116,"thread":"build-31"},{"duration":2,"stepId":"io.quarkus.smallrye.openapi.deployment.SmallRyeOpenApiProcessor#registerAutoSecurityFilter","started":"16:23:20.827","dependents":[439,368,366,367],"id":349,"thread":"build-44"},{"duration":2,"stepId":"io.quarkus.vertx.http.deployment.devmode.ArcDevProcessor#registerRoutes","started":"16:23:20.973","dependents":[388,439,430,431,436],"id":381,"thread":"build-42"},{"duration":2,"stepId":"io.quarkus.arc.deployment.ArcProcessor#marker","started":"16:23:20.187","dependents":[271],"id":117,"thread":"build-30"},{"duration":2,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#initializeHttpSecurity","started":"16:23:21.088","dependents":[439,433,398,432],"id":396,"thread":"build-2"},{"duration":2,"stepId":"io.quarkus.resteasy.reactive.common.deployment.ResteasyReactiveCommonProcessor#buildResourceInterceptors","started":"16:23:20.748","dependents":[346,352,337,415,408,419],"id":329,"thread":"build-40"},{"duration":2,"stepId":"io.quarkus.arc.deployment.BuildTimeEnabledProcessor#ifBuildProperty","started":"16:23:20.720","dependents":[306,299,298],"id":293,"thread":"build-32"},{"duration":2,"stepId":"io.quarkus.resteasy.reactive.jackson.deployment.processor.ResteasyReactiveJacksonProcessor#beans","started":"16:23:20.172","dependents":[352,337],"id":42,"thread":"build-17"},{"duration":2,"stepId":"io.quarkus.jackson.deployment.JacksonProcessor#unremovable","started":"16:23:20.718","dependents":[352,337,373,379],"id":286,"thread":"build-38"},{"duration":2,"stepId":"io.quarkus.arc.deployment.ArcProcessor#notifyBeanContainerListeners","started":"16:23:21.083","dependents":[439,391],"id":390,"thread":"build-81"},{"duration":2,"stepId":"ai.timefold.solver.quarkus.deployment.TimefoldProcessor#recordAndRegisterRuntimeBeans","started":"16:23:20.785","dependents":[439,368,366,367],"id":335,"thread":"build-6"},{"duration":2,"stepId":"io.quarkus.vertx.http.deployment.StaticResourcesProcessor#runtimeInit","started":"16:23:21.087","dependents":[439,433],"id":393,"thread":"build-56"},{"duration":2,"stepId":"io.quarkus.deployment.steps.DevServicesConfigBuildStep#deprecated","started":"16:23:20.188","dependents":[207],"id":125,"thread":"build-11"},{"duration":2,"stepId":"io.quarkus.arc.deployment.SyntheticBeansProcessor#initStatic","started":"16:23:20.939","dependents":[439,370],"id":367,"thread":"build-51"},{"duration":2,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#vertxIntegration","started":"16:23:20.165","dependents":[417,415,414],"id":21,"thread":"build-4"},{"duration":2,"stepId":"io.quarkus.smallrye.openapi.deployment.devui.OpenApiDevUIProcessor#pages","started":"16:23:20.198","dependents":[400,387],"id":158,"thread":"build-13"},{"duration":2,"stepId":"io.quarkus.arc.deployment.ConfigStaticInitBuildSteps#transformConfigProducer","started":"16:23:20.183","dependents":[352],"id":101,"thread":"build-17"},{"duration":2,"stepId":"io.quarkus.arc.deployment.ConfigBuildStep#registerConfigClasses","started":"16:23:20.974","dependents":[439],"id":383,"thread":"build-34"},{"duration":2,"stepId":"io.quarkus.vertx.deployment.VertxProcessor#collectEventConsumers","started":"16:23:20.932","dependents":[364,370],"id":361,"thread":"build-34"},{"duration":2,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#transformEndpoints","started":"16:23:20.823","dependents":[352],"id":346,"thread":"build-62"},{"duration":2,"stepId":"io.quarkus.devui.deployment.logstream.LogStreamProcessor#registerBuildTimeActions","started":"16:23:20.181","dependents":[264],"id":75,"thread":"build-30"},{"duration":2,"stepId":"io.quarkus.arc.deployment.ArcProcessor#exposeCustomScopeNames","started":"16:23:20.187","dependents":[167,164,357,352,337,345,343,228],"id":118,"thread":"build-27"},{"duration":2,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#registerSafeDuplicatedContextInterceptor","started":"16:23:20.187","dependents":[352,337],"id":119,"thread":"build-24"},{"duration":2,"stepId":"io.quarkus.devui.deployment.BuildTimeContentProcessor#mapPageBuildTimeData","started":"16:23:21.018","dependents":[426],"id":387,"thread":"build-82"},{"duration":2,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#additionalAsyncTypeMethodScanners","started":"16:23:20.161","dependents":[408],"id":7,"thread":"build-4"},{"duration":2,"stepId":"io.quarkus.arc.deployment.devui.ArcDevUIProcessor#createJsonRPCService","started":"16:23:20.182","dependents":[235,300,241],"id":90,"thread":"build-29"},{"duration":2,"stepId":"io.quarkus.arc.deployment.ArcProcessor#initializeContainer","started":"16:23:21.080","dependents":[439,390],"id":389,"thread":"build-80"},{"duration":2,"stepId":"io.quarkus.vertx.deployment.VertxProcessor#build","started":"16:23:20.934","dependents":[365,439,434],"id":364,"thread":"build-60"},{"duration":1,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveDevModeProcessor#openCommand","started":"16:23:21.144","dependents":[412],"id":411,"thread":"build-57"},{"duration":1,"stepId":"io.quarkus.smallrye.openapi.deployment.SmallRyeOpenApiProcessor#registerOpenApiSchemaClassesForReflection","started":"16:23:20.828","dependents":[416,438],"id":348,"thread":"build-51"},{"duration":1,"stepId":"io.quarkus.deployment.JniProcessor#setupJni","started":"16:23:20.206","dependents":[234],"id":165,"thread":"build-59"},{"duration":1,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveCDIProcessor#additionalBeans","started":"16:23:20.731","dependents":[352,337,438],"id":324,"thread":"build-6"},{"duration":1,"stepId":"io.quarkus.devui.deployment.menu.ContinuousTestingProcessor#createJsonRPCService","started":"16:23:20.162","dependents":[235,300,241],"id":2,"thread":"build-5"},{"duration":1,"stepId":"io.quarkus.smallrye.openapi.deployment.SmallRyeOpenApiProcessor#configFiles","started":"16:23:20.234","dependents":[238],"id":189,"thread":"build-15"},{"duration":1,"stepId":"io.quarkus.vertx.deployment.VertxProcessor#unremovableBeans","started":"16:23:20.163","dependents":[373,379],"id":9,"thread":"build-6"},{"duration":1,"stepId":"io.quarkus.resteasy.reactive.jackson.deployment.processor.ResteasyReactiveJacksonProcessor#additionalProviders","started":"16:23:21.145","dependents":[417,415,414],"id":413,"thread":"build-83"},{"duration":1,"stepId":"io.quarkus.arc.deployment.ArcProcessor#registerSyntheticObservers","started":"16:23:20.947","dependents":[388,372,373,371,438,379],"id":370,"thread":"build-40"},{"duration":1,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#build_9d6b7122fb368970c50c3a870d1f672392cd8afb","started":"16:23:20.180","dependents":[234,438],"id":66,"thread":"build-14"},{"duration":1,"stepId":"io.quarkus.vertx.http.deployment.ManagementInterfaceSecurityProcessor#setupAuthenticationMechanisms","started":"16:23:20.257","dependents":[439,433,352,337],"id":211,"thread":"build-22"},{"duration":1,"stepId":"io.quarkus.vertx.deployment.EventConsumerMethodsProcessor#eventConsumerMethods","started":"16:23:20.161","dependents":[342],"id":1,"thread":"build-3"},{"duration":1,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor#compressionSupport","started":"16:23:20.195","dependents":[408],"id":152,"thread":"build-19"},{"duration":1,"stepId":"io.quarkus.devui.deployment.menu.ContinuousTestingProcessor#continuousTestingState","started":"16:23:21.087","dependents":[439],"id":392,"thread":"build-78"},{"duration":1,"stepId":"io.quarkus.arc.deployment.AutoProducerMethodsProcessor#annotationTransformer","started":"16:23:20.823","dependents":[352],"id":345,"thread":"build-20"},{"duration":1,"stepId":"io.quarkus.netty.deployment.NettyProcessor#cleanupUnsafeLog","started":"16:23:20.168","dependents":[93,332],"id":27,"thread":"build-13"},{"duration":1,"stepId":"io.quarkus.arc.deployment.ArcProcessor#unremovableAsyncObserverExceptionHandlers","started":"16:23:20.167","dependents":[373,379],"id":22,"thread":"build-11"},{"duration":1,"stepId":"io.quarkus.arc.deployment.AutoAddScopeProcessor#annotationTransformer","started":"16:23:20.823","dependents":[352,373,379],"id":343,"thread":"build-51"},{"duration":1,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#detectBasicAuthImplicitlyRequired","started":"16:23:20.933","dependents":[439],"id":360,"thread":"build-60"},{"duration":1,"stepId":"io.quarkus.devui.deployment.logstream.LogStreamProcessor#handler","started":"16:23:20.714","dependents":[439,332],"id":274,"thread":"build-6"},{"duration":1,"stepId":"io.quarkus.devui.deployment.menu.ContinuousTestingProcessor#createContinuousTestingPages","started":"16:23:20.171","dependents":[404],"id":32,"thread":"build-16"},{"duration":1,"stepId":"io.quarkus.smallrye.openapi.deployment.SmallRyeOpenApiProcessor#handler","started":"16:23:21.173","dependents":[439,436,425],"id":424,"thread":"build-90"},{"duration":1,"stepId":"io.quarkus.swaggerui.deployment.SwaggerUiProcessor#feature","started":"16:23:20.194","dependents":[439],"id":146,"thread":"build-44"},{"duration":1,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#initializeRouter","started":"16:23:21.281","dependents":[439,436,433,432],"id":431,"thread":"build-90"},{"duration":1,"stepId":"io.quarkus.devui.deployment.menu.ContinuousTestingProcessor#registerBuildTimeActions","started":"16:23:20.185","dependents":[264],"id":102,"thread":"build-35"},{"duration":1,"stepId":"io.quarkus.arc.deployment.ReflectiveBeanClassesProcessor#implicitReflectiveBeanClasses","started":"16:23:20.933","dependents":[388],"id":359,"thread":"build-51"},{"duration":1,"stepId":"io.quarkus.arc.deployment.staticmethods.InterceptedStaticMethodsProcessor#callInitializer","started":"16:23:21.088","dependents":[439],"id":394,"thread":"build-81"},{"duration":1,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#setMtlsCertificateRoleProperties","started":"16:23:21.090","dependents":[439],"id":398,"thread":"build-57"},{"duration":1,"stepId":"io.quarkus.deployment.steps.RegisterForReflectionBuildStep#build","started":"16:23:20.720","dependents":[416,438],"id":288,"thread":"build-8"},{"duration":1,"stepId":"io.quarkus.deployment.steps.ShutdownListenerBuildStep#setupShutdown","started":"16:23:21.287","dependents":[439],"id":435,"thread":"build-83"},{"duration":1,"stepId":"io.quarkus.deployment.dev.testing.TestTracingProcessor#handle","started":"16:23:20.171","dependents":[93,332],"id":36,"thread":"build-4"},{"duration":1,"stepId":"io.quarkus.arc.deployment.WrongAnnotationUsageProcessor#detect","started":"16:23:20.932","dependents":[388],"id":357,"thread":"build-42"},{"duration":1,"stepId":"io.quarkus.arc.deployment.ConfigBuildStep#registerConfigMappingsBean","started":"16:23:20.932","dependents":[370],"id":358,"thread":"build-2"},{"duration":1,"stepId":"io.quarkus.swaggerui.deployment.SwaggerUiProcessor#registerSwaggerUiHandler","started":"16:23:21.132","dependents":[439,430,431],"id":407,"thread":"build-90"},{"duration":1,"stepId":"io.quarkus.tls.deployment.CertificatesProcessor#initializeCertificate","started":"16:23:20.937","dependents":[439,368,366,396,433,367],"id":365,"thread":"build-34"},{"duration":1,"stepId":"io.quarkus.deployment.logging.LoggingResourceProcessor#setupLogFilters","started":"16:23:20.162","dependents":[93,332],"id":3,"thread":"build-6"},{"duration":1,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#securityExceptionMappers","started":"16:23:20.167","dependents":[331],"id":25,"thread":"build-7"},{"duration":1,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#openSocket","started":"16:23:21.287","dependents":[439,438],"id":437,"thread":"build-2"},{"duration":1,"stepId":"io.quarkus.arc.deployment.AutoInjectFieldProcessor#annotationTransformer","started":"16:23:20.824","dependents":[352],"id":344,"thread":"build-6"},{"duration":1,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#createVertxContextHandlers","started":"16:23:20.285","dependents":[243,439,246],"id":242,"thread":"build-44"},{"duration":1,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#logging","started":"16:23:20.170","dependents":[53],"id":33,"thread":"build-11"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#integrateEagerSecurity","started":"16:23:20.729","dependents":[408],"id":308,"thread":"build-56"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.common.deployment.ResteasyReactiveCommonProcessor#deprioritizeLegacyProviders","started":"16:23:20.166","dependents":[417],"id":16,"thread":"build-11"},{"duration":0,"stepId":"io.quarkus.arc.deployment.BuildTimeEnabledProcessor#unlessBuildProperty","started":"16:23:20.722","dependents":[306,299,298],"id":289,"thread":"build-66"},{"duration":0,"stepId":"io.quarkus.vertx.deployment.VertxProcessor#registerVerticleClasses","started":"16:23:20.722","dependents":[438],"id":291,"thread":"build-56"},{"duration":0,"stepId":"io.quarkus.devui.deployment.menu.DependenciesProcessor#createBuildTimeActions","started":"16:23:20.209","dependents":[264],"id":170,"thread":"build-64"},{"duration":0,"stepId":"io.quarkus.arc.deployment.BuildTimeEnabledProcessor#findEnablementStereotypes","started":"16:23:20.720","dependents":[283,293,289,292],"id":280,"thread":"build-33"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.jackson.deployment.processor.ResteasyReactiveJacksonProcessor#handleJsonAnnotations","started":"16:23:21.144","dependents":[413,439,438],"id":410,"thread":"build-82"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#addDefaultAuthFailureHandler","started":"16:23:21.173","dependents":[439,433,424],"id":423,"thread":"build-2"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#runtimeConfiguration","started":"16:23:21.172","dependents":[422,439],"id":421,"thread":"build-83"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#gatherAuthorizationPolicyInstances","started":"16:23:20.719","dependents":[296,308],"id":278,"thread":"build-20"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#produceEagerSecurityInterceptorStorage","started":"16:23:20.732","dependents":[439,368,366,367],"id":320,"thread":"build-34"},{"duration":0,"stepId":"io.quarkus.deployment.console.ConsoleProcessor#installCliCommands","started":"16:23:21.145","dependents":[434],"id":412,"thread":"build-82"},{"duration":0,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#cleanupVertxWarnings","started":"16:23:20.165","dependents":[93,332],"id":12,"thread":"build-8"},{"duration":0,"stepId":"io.quarkus.arc.deployment.BuildTimeEnabledProcessor#buildExclusions","started":"16:23:20.725","dependents":[347],"id":298,"thread":"build-42"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ConfigBuildStep#additionalBeans","started":"16:23:20.176","dependents":[352,337],"id":48,"thread":"build-21"},{"duration":0,"stepId":"io.quarkus.deployment.logging.LoggingResourceProcessor#closeBuildTimeLogging","started":"16:23:20.181","dependents":[434],"id":68,"thread":"build-33"},{"duration":0,"stepId":"io.quarkus.deployment.steps.ConfigGenerationBuildStep#setupConfigOverride","started":"16:23:20.187","dependents":[],"id":106,"thread":"build-17"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#convertRoutes","started":"16:23:21.175","dependents":[430,431],"id":425,"thread":"build-2"},{"duration":0,"stepId":"io.quarkus.deployment.SecureRandomProcessor#registerReflectiveMethods","started":"16:23:20.173","dependents":[438],"id":40,"thread":"build-4"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor#scanForParamConverters_dcdfdd2a310a09abe5ee3f0ed2b2bc49f36f3d07","started":"16:23:20.730","dependents":[352,337,408,438,419],"id":316,"thread":"build-50"},{"duration":0,"stepId":"io.quarkus.deployment.console.ConsoleProcessor#setupExceptionHandler","started":"16:23:20.364","dependents":[273],"id":267,"thread":"build-6"},{"duration":0,"stepId":"io.quarkus.deployment.steps.ProfileBuildStep#defaultProfile","started":"16:23:20.189","dependents":[384],"id":121,"thread":"build-5"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveCDIProcessor#requestScopedResources","started":"16:23:20.732","dependents":[352],"id":322,"thread":"build-40"},{"duration":0,"stepId":"io.quarkus.deployment.CollectionClassProcessor#setupCollectionClasses","started":"16:23:20.171","dependents":[438],"id":30,"thread":"build-13"},{"duration":0,"stepId":"io.quarkus.deployment.steps.CurateOutcomeBuildStep#removeResources","started":"16:23:20.209","dependents":[418],"id":168,"thread":"build-31"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ConfigBuildStep#validateConfigMappingsInjectionPoints","started":"16:23:20.973","dependents":[384,383],"id":379,"thread":"build-20"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ConfigBuildStep#vetoMPConfigProperties","started":"16:23:20.165","dependents":[352],"id":14,"thread":"build-7"},{"duration":0,"stepId":"io.quarkus.smallrye.context.deployment.SmallRyeContextPropagationProcessor#registerBean","started":"16:23:20.183","dependents":[352,337],"id":74,"thread":"build-33"},{"duration":0,"stepId":"io.quarkus.devui.deployment.BuildTimeContentProcessor#mapDeploymentMethods","started":"16:23:20.344","dependents":[300,397],"id":264,"thread":"build-8"},{"duration":0,"stepId":"io.quarkus.devui.deployment.menu.EndpointsProcessor#createEndpointsPage","started":"16:23:20.221","dependents":[404],"id":178,"thread":"build-43"},{"duration":0,"stepId":"io.quarkus.deployment.ide.IdeProcessor#effectiveIde","started":"16:23:20.192","dependents":[273,404,267,193],"id":136,"thread":"build-19"},{"duration":0,"stepId":"io.quarkus.deployment.steps.AdditionalClassLoaderResourcesBuildStep#appendAdditionalClassloaderResources","started":"16:23:20.180","dependents":[275],"id":57,"thread":"build-29"},{"duration":0,"stepId":"io.quarkus.devui.deployment.BuildTimeContentProcessor#createKnownInternalImportMap","started":"16:23:20.197","dependents":[158,270,427],"id":154,"thread":"build-44"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.common.deployment.JaxrsMethodsProcessor#jaxrsMethods","started":"16:23:20.823","dependents":[342],"id":340,"thread":"build-42"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ShutdownBuildSteps#registerShutdownObservers","started":"16:23:20.949","dependents":[373],"id":371,"thread":"build-60"},{"duration":0,"stepId":"io.quarkus.deployment.index.ApplicationArchiveBuildStep#addConfiguredIndexedDependencies","started":"16:23:20.193","dependents":[271],"id":140,"thread":"build-21"},{"duration":0,"stepId":"io.quarkus.deployment.logging.LoggingResourceProcessor#setMinLevelForInitialConfigurator","started":"16:23:20.193","dependents":[439],"id":143,"thread":"build-43"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ArcProcessor#quarkusApplication","started":"16:23:20.721","dependents":[352,337],"id":284,"thread":"build-21"},{"duration":0,"stepId":"io.quarkus.deployment.execannotations.ExecutionModelAnnotationsProcessor#check","started":"16:23:20.824","dependents":[],"id":342,"thread":"build-44"},{"duration":0,"stepId":"io.quarkus.arc.deployment.init.InitializationTaskProcessor#startApplicationInitializer","started":"16:23:20.947","dependents":[439],"id":369,"thread":"build-51"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#unremovableBeans","started":"16:23:20.734","dependents":[373,379],"id":327,"thread":"build-6"},{"duration":0,"stepId":"io.quarkus.deployment.logging.LoggingResourceProcessor#setProperty","started":"16:23:20.185","dependents":[439],"id":94,"thread":"build-11"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveCDIProcessor#perClassExceptionMapperSupport","started":"16:23:20.730","dependents":[352],"id":309,"thread":"build-27"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor#applicationSpecificUnwrappedExceptions","started":"16:23:20.720","dependents":[331],"id":281,"thread":"build-41"},{"duration":0,"stepId":"io.quarkus.arc.deployment.AutoInjectFieldProcessor#autoInjectQualifiers","started":"16:23:20.823","dependents":[344,343],"id":338,"thread":"build-40"},{"duration":0,"stepId":"io.quarkus.deployment.logging.LoggingResourceProcessor#setUpDarkeningDefault","started":"16:23:20.188","dependents":[384],"id":113,"thread":"build-39"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor#scanForFeatures","started":"16:23:20.731","dependents":[324,419],"id":319,"thread":"build-2"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#addAllWriteableMarker","started":"16:23:21.147","dependents":[418],"id":414,"thread":"build-7"},{"duration":0,"stepId":"io.quarkus.deployment.logging.LoggingWithPanacheProcessor#process","started":"16:23:20.721","dependents":[418],"id":282,"thread":"build-47"},{"duration":0,"stepId":"io.quarkus.devui.deployment.ReportIssuesProcessor#registerJsonRpcService","started":"16:23:20.167","dependents":[235,300,241],"id":19,"thread":"build-5"},{"duration":0,"stepId":"io.quarkus.webdependency.locator.deployment.WebDependencyLocatorProcessor#findRelevantFiles","started":"16:23:20.195","dependents":[238,197,188],"id":148,"thread":"build-20"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#setupAuthenticationMechanisms","started":"16:23:20.267","dependents":[439,351,433,352,337,424,349],"id":225,"thread":"build-66"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#recordableConstructor","started":"16:23:20.183","dependents":[439],"id":76,"thread":"build-14"},{"duration":0,"stepId":"io.quarkus.deployment.console.ConsoleProcessor#missingDevUIMessageHandler","started":"16:23:20.364","dependents":[434],"id":268,"thread":"build-8"},{"duration":0,"stepId":"io.quarkus.deployment.dev.testing.TestTracingProcessor#sharedStateListener","started":"16:23:20.173","dependents":[269],"id":39,"thread":"build-11"},{"duration":0,"stepId":"ai.timefold.solver.quarkus.deployment.TimefoldProcessor#makeSolverFactoryUnremovableInDevMode","started":"16:23:20.192","dependents":[373,379],"id":135,"thread":"build-41"},{"duration":0,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#preventLoggerContention","started":"16:23:20.173","dependents":[53],"id":38,"thread":"build-5"},{"duration":0,"stepId":"io.quarkus.deployment.ForkJoinPoolProcessor#setProperty","started":"16:23:20.184","dependents":[439],"id":86,"thread":"build-30"},{"duration":0,"stepId":"io.quarkus.mutiny.deployment.MutinyProcessor#runtimeInit","started":"16:23:20.293","dependents":[439],"id":246,"thread":"build-41"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.StaticResourcesProcessor#indexHtmlFile","started":"16:23:20.190","dependents":[238],"id":124,"thread":"build-22"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#generateCustomProducer","started":"16:23:20.734","dependents":[352,337],"id":326,"thread":"build-6"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.jackson.deployment.processor.ResteasyReactiveJacksonProcessor#initializeRolesAllowedConfigExp","started":"16:23:20.973","dependents":[439],"id":375,"thread":"build-34"},{"duration":0,"stepId":"io.quarkus.devui.deployment.BuildTimeContentProcessor#loadAllBuildTimeTemplates","started":"16:23:21.243","dependents":[429],"id":428,"thread":"build-6"},{"duration":0,"stepId":"io.quarkus.vertx.deployment.VertxProcessor#featureAndCapability","started":"16:23:20.179","dependents":[439,175],"id":52,"thread":"build-26"},{"duration":0,"stepId":"io.quarkus.arc.deployment.SyntheticBeansProcessor#initRegular","started":"16:23:20.939","dependents":[370],"id":366,"thread":"build-40"},{"duration":0,"stepId":"io.quarkus.arc.deployment.BuildTimeEnabledProcessor#unlessBuildProfile","started":"16:23:20.720","dependents":[306,299,298],"id":283,"thread":"build-11"},{"duration":0,"stepId":"io.quarkus.smallrye.openapi.deployment.devui.OpenApiDevUIProcessor#createJsonRPCService","started":"16:23:20.187","dependents":[235,300,241],"id":105,"thread":"build-11"},{"duration":0,"stepId":"io.quarkus.devui.deployment.menu.MCPProcessor#createMCPPage","started":"16:23:20.176","dependents":[400,387],"id":47,"thread":"build-17"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#initMtlsClientAuth","started":"16:23:20.202","dependents":[352,337],"id":161,"thread":"build-18"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor#scanForDynamicFeatures","started":"16:23:20.730","dependents":[324,419],"id":315,"thread":"build-8"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#config","started":"16:23:20.164","dependents":[384],"id":6,"thread":"build-8"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ArcProcessor#registerContextPropagation","started":"16:23:20.193","dependents":[229],"id":141,"thread":"build-13"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#generateAuthorizationPolicyStorage","started":"16:23:20.724","dependents":[337],"id":296,"thread":"build-61"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ArcProcessor#feature","started":"16:23:20.180","dependents":[439],"id":60,"thread":"build-30"},{"duration":0,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#registerVerticleClasses","started":"16:23:20.720","dependents":[438],"id":285,"thread":"build-53"},{"duration":0,"stepId":"io.quarkus.arc.deployment.LookupConditionsProcessor#suppressConditionsGenerators","started":"16:23:20.823","dependents":[352],"id":341,"thread":"build-60"},{"duration":0,"stepId":"io.quarkus.swaggerui.deployment.SwaggerUiProcessor#brandingFiles","started":"16:23:20.180","dependents":[238],"id":61,"thread":"build-18"},{"duration":0,"stepId":"io.quarkus.webdependency.locator.deployment.WebDependencyLocatorProcessor#feature","started":"16:23:20.166","dependents":[439],"id":17,"thread":"build-5"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ConfigBuildStep#validateStaticInitConfigProperty","started":"16:23:20.974","dependents":[439,438],"id":380,"thread":"build-2"},{"duration":0,"stepId":"io.quarkus.deployment.pkg.steps.FileSystemResourcesBuildStep#notNormalMode","started":"16:23:20.235","dependents":[],"id":187,"thread":"build-12"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ConfigBuildStep#generateConfigProperties","started":"16:23:20.719","dependents":[356,376,358,438,379],"id":279,"thread":"build-11"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#aggregateParameterContainers","started":"16:23:20.733","dependents":[326,346,327,408],"id":325,"thread":"build-40"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ExecutorServiceProcessor#executorServiceBean","started":"16:23:20.296","dependents":[368,366,367],"id":248,"thread":"build-11"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#buildSetup","started":"16:23:20.163","dependents":[439],"id":4,"thread":"build-7"},{"duration":0,"stepId":"io.quarkus.arc.deployment.LifecycleEventsBuildStep#startupEvent","started":"16:23:21.287","dependents":[437,439],"id":434,"thread":"build-90"},{"duration":0,"stepId":"io.quarkus.smallrye.openapi.deployment.SmallRyeOpenApiProcessor#logCleanup","started":"16:23:20.175","dependents":[93,332],"id":44,"thread":"build-16"},{"duration":0,"stepId":"io.quarkus.deployment.ConstructorPropertiesProcessor#build","started":"16:23:20.724","dependents":[438],"id":295,"thread":"build-14"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#initFormAuth","started":"16:23:21.284","dependents":[439,434],"id":432,"thread":"build-83"},{"duration":0,"stepId":"io.quarkus.arc.deployment.StartupBuildSteps#unremovableBeans","started":"16:23:20.184","dependents":[373,379],"id":81,"thread":"build-21"},{"duration":0,"stepId":"io.quarkus.devui.deployment.menu.BuildMetricsProcessor#createJsonRPCService","started":"16:23:20.180","dependents":[235,300,241],"id":59,"thread":"build-31"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveCDIProcessor#pathInterfaceImpls","started":"16:23:20.731","dependents":[352,337],"id":318,"thread":"build-6"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ConfigBuildStep#validateRuntimeConfigProperty","started":"16:23:20.976","dependents":[439,438],"id":382,"thread":"build-44"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#configureHandlers","started":"16:23:21.173","dependents":[439],"id":422,"thread":"build-90"},{"duration":0,"stepId":"io.quarkus.devui.deployment.menu.BuildMetricsProcessor#createBuildMetricsPages","started":"16:23:20.188","dependents":[404],"id":110,"thread":"build-22"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor#scanForInterceptors","started":"16:23:20.732","dependents":[329],"id":321,"thread":"build-51"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor#cacheControlSupport","started":"16:23:20.187","dependents":[408],"id":108,"thread":"build-14"},{"duration":0,"stepId":"io.quarkus.vertx.deployment.VertxProcessor#registerBean","started":"16:23:20.170","dependents":[352,337],"id":29,"thread":"build-16"},{"duration":0,"stepId":"io.quarkus.deployment.ExtensionLoader#booleanSupplierFactory","started":"16:23:20.184","dependents":[174],"id":83,"thread":"build-34"},{"duration":0,"stepId":"io.quarkus.netty.deployment.NettyProcessor#registerEventLoopBeans","started":"16:23:20.338","dependents":[439,368,366,367],"id":259,"thread":"build-27"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.jackson.deployment.processor.ResteasyReactiveJacksonProcessor#feature","started":"16:23:20.169","dependents":[439],"id":24,"thread":"build-11"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.devmode.NotFoundProcessor#resourceNotFoundDataAvailable","started":"16:23:20.183","dependents":[352,337],"id":80,"thread":"build-32"},{"duration":0,"stepId":"io.quarkus.devui.deployment.menu.MCPProcessor#createMCPJsonRPCService","started":"16:23:20.182","dependents":[235,300,241],"id":73,"thread":"build-31"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#registerSecurityBeans","started":"16:23:20.222","dependents":[352,337],"id":179,"thread":"build-55"},{"duration":0,"stepId":"io.quarkus.devui.deployment.welcome.WelcomeProcessor#createWelcomePages","started":"16:23:21.108","dependents":[404],"id":401,"thread":"build-57"},{"duration":0,"stepId":"io.quarkus.deployment.steps.ConfigGenerationBuildStep#suppressNonRuntimeConfigChanged","started":"16:23:20.186","dependents":[201],"id":99,"thread":"build-22"},{"duration":0,"stepId":"io.quarkus.devui.deployment.BuildTimeContentProcessor#createRelocationMap","started":"16:23:20.188","dependents":[427],"id":111,"thread":"build-17"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#makeRequiredBeansUnremovable","started":"16:23:20.221","dependents":[373,379],"id":176,"thread":"build-13"},{"duration":0,"stepId":"io.quarkus.devui.deployment.menu.ExtensionsProcessor#createExtensionsPages","started":"16:23:21.109","dependents":[404],"id":402,"thread":"build-83"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveVertxWebSocketIntegrationProcessor#scanner","started":"16:23:20.164","dependents":[408],"id":8,"thread":"build-9"},{"duration":0,"stepId":"io.quarkus.arc.deployment.UnremovableAnnotationsProcessor#unremovableBeans","started":"16:23:20.187","dependents":[373,379],"id":107,"thread":"build-11"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.common.deployment.ResteasyReactiveCommonProcessor#searchForProviders","started":"16:23:20.226","dependents":[271],"id":180,"thread":"build-64"},{"duration":0,"stepId":"io.quarkus.devui.deployment.logstream.LogStreamProcessor#additionalBean","started":"16:23:20.189","dependents":[352,337],"id":115,"thread":"build-35"},{"duration":0,"stepId":"io.quarkus.jackson.deployment.JacksonProcessor#register","started":"16:23:20.722","dependents":[416,352,337,438],"id":294,"thread":"build-51"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.jackson.deployment.processor.ResteasyReactiveJacksonProcessor#exceptionMappers","started":"16:23:20.187","dependents":[331],"id":109,"thread":"build-35"},{"duration":0,"stepId":"io.quarkus.deployment.execannotations.ExecutionModelAnnotationsProcessor#devuiJsonRpcServices","started":"16:23:20.186","dependents":[342],"id":98,"thread":"build-28"},{"duration":0,"stepId":"io.quarkus.jackson.deployment.JacksonProcessor#supportMixins","started":"16:23:20.721","dependents":[439,368,366,367,438],"id":287,"thread":"build-39"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#handleClassLevelExceptionMappers","started":"16:23:20.728","dependents":[408,438],"id":304,"thread":"build-56"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#registerHttpAuthMechanismAnnotations","started":"16:23:20.165","dependents":[303],"id":13,"thread":"build-6"},{"duration":0,"stepId":"io.quarkus.deployment.steps.MainClassBuildStep#applicationReflection","started":"16:23:20.184","dependents":[438],"id":85,"thread":"build-31"},{"duration":0,"stepId":"io.quarkus.arc.deployment.TestsAsBeansProcessor#testAnnotations","started":"16:23:20.186","dependents":[352,337,228],"id":100,"thread":"build-14"},{"duration":0,"stepId":"io.quarkus.deployment.steps.MainClassBuildStep#setupVersionField","started":"16:23:20.165","dependents":[438],"id":10,"thread":"build-5"},{"duration":0,"stepId":"io.quarkus.devui.deployment.menu.ReadmeProcessor#createJsonRPCServiceForCache","started":"16:23:20.180","dependents":[235,300,241],"id":56,"thread":"build-18"},{"duration":0,"stepId":"io.quarkus.devui.deployment.logstream.LogStreamProcessor#createJsonRPCService","started":"16:23:20.167","dependents":[235,300,241],"id":20,"thread":"build-10"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.common.deployment.ResteasyReactiveCommonProcessor#resourceIndex","started":"16:23:20.721","dependents":[409,337,302],"id":290,"thread":"build-42"},{"duration":0,"stepId":"io.quarkus.netty.deployment.NettyProcessor#limitArenaSize","started":"16:23:20.196","dependents":[439],"id":150,"thread":"build-47"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ArcProcessor#validateAsyncObserverExceptionHandlers","started":"16:23:20.973","dependents":[388],"id":377,"thread":"build-2"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ObservabilityProcessor#preAuthFailureFilter","started":"16:23:21.172","dependents":[439,423,433,424],"id":420,"thread":"build-90"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.ManagementInterfaceSecurityProcessor#initializeAuthMechanismHandler","started":"16:23:21.089","dependents":[439,433],"id":395,"thread":"build-57"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#setMinimalNettyMaxOrderSize","started":"16:23:20.174","dependents":[214,150],"id":43,"thread":"build-16"},{"duration":0,"stepId":"io.quarkus.arc.deployment.TestsAsBeansProcessor#testClassBeans","started":"16:23:20.186","dependents":[352,337],"id":103,"thread":"build-13"},{"duration":0,"stepId":"io.quarkus.deployment.steps.ConfigGenerationBuildStep#watchConfigFiles","started":"16:23:20.183","dependents":[238],"id":88,"thread":"build-4"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#httpRoot","started":"16:23:20.196","dependents":[227,436,429,433,425,403,411],"id":151,"thread":"build-44"},{"duration":0,"stepId":"io.quarkus.deployment.ide.IdeProcessor#detectIdeFiles","started":"16:23:20.188","dependents":[136],"id":114,"thread":"build-22"},{"duration":0,"stepId":"io.quarkus.deployment.steps.PreloadClassesBuildStep#registerPreInitClasses","started":"16:23:20.166","dependents":[],"id":15,"thread":"build-7"},{"duration":0,"stepId":"io.quarkus.smallrye.context.deployment.SmallRyeContextPropagationProcessor#createSynthBeansForConfiguredInjectionPoints","started":"16:23:20.932","dependents":[439,368,366,367],"id":354,"thread":"build-20"},{"duration":0,"stepId":"io.quarkus.devui.deployment.menu.EndpointsProcessor#createJsonRPCService","started":"16:23:20.173","dependents":[235,300,241],"id":41,"thread":"build-20"},{"duration":0,"stepId":"io.quarkus.tls.deployment.CertificatesProcessor#unremovableBeans","started":"16:23:20.181","dependents":[373,379],"id":65,"thread":"build-29"},{"duration":0,"stepId":"io.quarkus.devui.deployment.DevUIProcessor#createAllRoutes","started":"16:23:21.132","dependents":[429],"id":406,"thread":"build-83"},{"duration":0,"stepId":"io.quarkus.smallrye.context.deployment.SmallRyeContextPropagationProcessor#transformInjectionPoint","started":"16:23:20.180","dependents":[352],"id":62,"thread":"build-31"},{"duration":0,"stepId":"io.quarkus.vertx.deployment.VertxProcessor#registerReflectivelyAccessedMethods","started":"16:23:20.172","dependents":[438],"id":37,"thread":"build-18"},{"duration":0,"stepId":"io.quarkus.deployment.steps.BannerProcessor#watchBannerChanges","started":"16:23:20.192","dependents":[238],"id":134,"thread":"build-4"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#additionalBeans","started":"16:23:20.188","dependents":[352,337],"id":112,"thread":"build-17"},{"duration":0,"stepId":"ai.timefold.solver.quarkus.deployment.TimefoldProcessor#feature","started":"16:23:20.193","dependents":[439],"id":138,"thread":"build-21"},{"duration":0,"stepId":"io.quarkus.smallrye.openapi.deployment.SmallRyeOpenApiProcessor#contributeClassesToIndex","started":"16:23:20.181","dependents":[275],"id":63,"thread":"build-31"},{"duration":0,"stepId":"io.quarkus.vertx.deployment.EventBusCodecProcessor#registerCodecs","started":"16:23:20.823","dependents":[364,438],"id":339,"thread":"build-44"},{"duration":0,"stepId":"io.quarkus.devui.deployment.menu.BuildMetricsProcessor#additionalBeans","started":"16:23:20.182","dependents":[352,337],"id":72,"thread":"build-32"},{"duration":0,"stepId":"io.quarkus.arc.deployment.BuildTimeEnabledProcessor#ifBuildProfile","started":"16:23:20.722","dependents":[306,299,298],"id":292,"thread":"build-40"},{"duration":0,"stepId":"io.quarkus.devui.deployment.menu.WorkspaceProcessor#createWorkspacePage","started":"16:23:20.180","dependents":[404],"id":55,"thread":"build-18"},{"duration":0,"stepId":"io.quarkus.deployment.dev.ConfigureDisableInstrumentationBuildStep#configure","started":"16:23:20.185","dependents":[434],"id":95,"thread":"build-24"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ConfigBuildStep#registerCustomConfigBeanTypes","started":"16:23:20.932","dependents":[368,366,367,438],"id":355,"thread":"build-56"},{"duration":0,"stepId":"io.quarkus.arc.deployment.BuildTimeEnabledProcessor#conditionTransformer","started":"16:23:20.726","dependents":[352],"id":299,"thread":"build-56"},{"duration":0,"stepId":"ai.timefold.solver.quarkus.jackson.deployment.TimefoldJacksonProcessor#registerTimefoldJacksonModule","started":"16:23:20.163","dependents":[336],"id":5,"thread":"build-3"},{"duration":0,"stepId":"io.quarkus.deployment.steps.CurateOutcomeBuildStep#curateOutcome","started":"16:23:20.202","dependents":[213,400,273,300,169,294,173,174,245,426,401,247,404,170,215,271,250,264,175,387,418,209,237,168,405],"id":159,"thread":"build-13"},{"duration":0,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#doNotRemoveVertxOptionsCustomizers","started":"16:23:20.168","dependents":[373,379],"id":23,"thread":"build-4"},{"duration":0,"stepId":"io.quarkus.devui.deployment.build.BuildMetricsDevUIProcessor#createJsonRPCService","started":"16:23:20.186","dependents":[235,300,241],"id":104,"thread":"build-11"},{"duration":0,"stepId":"io.quarkus.arc.deployment.StartupBuildSteps#registerStartupObservers","started":"16:23:20.949","dependents":[373],"id":372,"thread":"build-51"},{"duration":0,"stepId":"io.quarkus.vertx.deployment.VertxProcessor#reinitializeClassesForNetty","started":"16:23:20.176","dependents":[234],"id":46,"thread":"build-4"},{"duration":0,"stepId":"io.quarkus.devui.deployment.menu.ReadmeProcessor#createReadmePage","started":"16:23:20.190","dependents":[404],"id":123,"thread":"build-27"},{"duration":0,"stepId":"io.quarkus.deployment.pkg.steps.NativeImageBuildStep#ignoreBuildPropertyChanges","started":"16:23:20.182","dependents":[201],"id":69,"thread":"build-22"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.jackson.deployment.processor.ResteasyReactiveJacksonProcessor#customExceptionMappers","started":"16:23:20.182","dependents":[328],"id":70,"thread":"build-18"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ArcProcessor#launchMode","started":"16:23:20.184","dependents":[352,337],"id":89,"thread":"build-14"},{"duration":0,"stepId":"ai.timefold.solver.quarkus.jackson.deployment.TimefoldJacksonProcessor#feature","started":"16:23:20.172","dependents":[439],"id":35,"thread":"build-19"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ObserverValidationProcessor#validateApplicationObserver","started":"16:23:20.973","dependents":[388],"id":374,"thread":"build-51"},{"duration":0,"stepId":"io.quarkus.deployment.steps.CapabilityAggregationStep#aggregateCapabilities","started":"16:23:20.220","dependents":[331,177,213,400,208,308,286,180,179,288,419,437,420,296,223,225,352,303,264,176,192,268,416,237,348,313,278,408,178,403,398,216],"id":175,"thread":"build-57"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ConfigBuildStep#validateConfigPropertiesInjectionPoints","started":"16:23:20.973","dependents":[383],"id":376,"thread":"build-44"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor#scanForContextResolvers","started":"16:23:20.729","dependents":[413,352,337,438,419],"id":311,"thread":"build-14"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveCDIProcessor#unremovableContextMethodParams","started":"16:23:20.730","dependents":[373,379],"id":310,"thread":"build-39"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor#scanForParamConverters_59e3169e3a646b7fcf3083416f558434b73816c5","started":"16:23:20.730","dependents":[316],"id":314,"thread":"build-61"},{"duration":0,"stepId":"io.quarkus.netty.deployment.NettyProcessor#disableFinalizers","started":"16:23:20.185","dependents":[439],"id":96,"thread":"build-35"},{"duration":0,"stepId":"io.quarkus.arc.deployment.HotDeploymentConfigBuildStep#configFile","started":"16:23:20.169","dependents":[238],"id":28,"thread":"build-11"},{"duration":0,"stepId":"io.quarkus.deployment.dev.IsolatedDevModeMain$AddApplicationClassPredicateBuildStep$1@136b2771","started":"16:23:20.181","dependents":[352,408],"id":64,"thread":"build-32"},{"duration":0,"stepId":"io.quarkus.devui.deployment.build.BuildMetricsDevUIProcessor#additionalBeans","started":"16:23:20.184","dependents":[352,337],"id":84,"thread":"build-22"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#notFoundRoutes","started":"16:23:21.281","dependents":[436],"id":430,"thread":"build-6"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveCDIProcessor#subResourcesAsBeans","started":"16:23:20.730","dependents":[352,337,373,379],"id":312,"thread":"build-15"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.VertxHttpProcessor#frameworkRoot","started":"16:23:20.194","dependents":[407,227,431,195,381,433,425,193,154,426,158,404,429,270,424,178,411,427],"id":144,"thread":"build-13"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.jackson.deployment.processor.ResteasyReactiveJacksonProcessor#handleFieldSecurity","started":"16:23:21.144","dependents":[410],"id":409,"thread":"build-83"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#providersFromClasspath","started":"16:23:20.177","dependents":[417,415,414],"id":50,"thread":"build-11"},{"duration":0,"stepId":"io.quarkus.jackson.deployment.JacksonProcessor#autoRegisterModules","started":"16:23:20.719","dependents":[336],"id":277,"thread":"build-61"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.jackson.deployment.processor.ResteasyReactiveJacksonProcessor#reflection","started":"16:23:20.171","dependents":[438],"id":31,"thread":"build-17"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ConfigBuildStep#configPropertyInjectionPoints","started":"16:23:20.973","dependents":[380,382,438],"id":378,"thread":"build-56"},{"duration":0,"stepId":"io.quarkus.netty.deployment.NettyProcessor#cleanupMacDNSInLog","started":"16:23:20.166","dependents":[93,332],"id":18,"thread":"build-5"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ConfigBuildStep#registerConfigPropertiesBean","started":"16:23:20.932","dependents":[370],"id":356,"thread":"build-44"},{"duration":0,"stepId":"io.quarkus.vertx.core.deployment.VertxCoreProcessor#filterNettyHostsFileParsingWarn","started":"16:23:20.175","dependents":[93,332],"id":45,"thread":"build-21"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ShutdownBuildSteps#unremovableBeans","started":"16:23:20.165","dependents":[373,379],"id":11,"thread":"build-10"},{"duration":0,"stepId":"io.quarkus.arc.deployment.staticmethods.InterceptedStaticMethodsProcessor#processInterceptedStaticMethods","started":"16:23:20.935","dependents":[438,418],"id":363,"thread":"build-34"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ObservabilityProcessor#methodScanner","started":"16:23:20.222","dependents":[408],"id":177,"thread":"build-58"},{"duration":0,"stepId":"io.quarkus.smallrye.openapi.deployment.SmallRyeOpenApiProcessor#addAutoOpenApiEndpointFilter","started":"16:23:20.197","dependents":[403],"id":153,"thread":"build-44"},{"duration":0,"stepId":"ai.timefold.solver.quarkus.deployment.TimefoldProcessor#registerDevUICard","started":"16:23:20.193","dependents":[400,387],"id":139,"thread":"build-5"},{"duration":0,"stepId":"io.quarkus.arc.deployment.ArcProcessor#signalBeanContainerReady","started":"16:23:21.086","dependents":[394,396,399,433,434,392,419,439,393,436,429,417,397,408,395],"id":391,"thread":"build-80"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor#defaultUnwrappedExceptions","started":"16:23:20.168","dependents":[331],"id":26,"thread":"build-5"},{"duration":0,"stepId":"io.quarkus.vertx.http.deployment.HttpSecurityProcessor#collectInterceptedMethods","started":"16:23:20.728","dependents":[308,320],"id":305,"thread":"build-42"},{"duration":0,"stepId":"io.quarkus.devui.deployment.menu.ConfigurationProcessor#registerJsonRpcService","started":"16:23:20.178","dependents":[235,300,241],"id":51,"thread":"build-23"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.common.deployment.ResteasyReactiveCommonProcessor#scanForIOInterceptors","started":"16:23:20.731","dependents":[329],"id":317,"thread":"build-32"},{"duration":0,"stepId":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#registerCustomExceptionMappers","started":"16:23:20.180","dependents":[328],"id":58,"thread":"build-30"},{"duration":0,"stepId":"io.quarkus.deployment.steps.ReflectionDiagnosticProcessor#writeReflectionData","started":"16:23:21.289","dependents":[],"id":438,"thread":"build-83"}],"started":"2025-09-20T16:23:20.16","items":[{"count":1146,"class":"io.quarkus.deployment.builditem.ConfigDescriptionBuildItem"},{"count":625,"class":"io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem"},{"count":395,"class":"io.quarkus.deployment.builditem.GeneratedClassBuildItem"},{"count":153,"class":"io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem"},{"count":94,"class":"io.quarkus.hibernate.validator.spi.AdditionalConstrainedClassBuildItem"},{"count":64,"class":"io.quarkus.deployment.builditem.MainBytecodeRecorderBuildItem"},{"count":49,"class":"io.quarkus.arc.deployment.AdditionalBeanBuildItem"},{"count":44,"class":"io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem"},{"count":40,"class":"io.quarkus.vertx.http.deployment.RouteBuildItem"},{"count":38,"class":"io.quarkus.deployment.builditem.StaticBytecodeRecorderBuildItem"},{"count":29,"class":"io.quarkus.deployment.builditem.ConfigClassBuildItem"},{"count":28,"class":"io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem"},{"count":24,"class":"io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem"},{"count":18,"class":"io.quarkus.arc.deployment.SyntheticBeanBuildItem"},{"count":17,"class":"io.quarkus.deployment.builditem.BytecodeTransformerBuildItem"},{"count":16,"class":"io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem"},{"count":15,"class":"io.quarkus.devui.spi.JsonRPCProvidersBuildItem"},{"count":14,"class":"io.quarkus.arc.deployment.UnremovableBeanBuildItem"},{"count":14,"class":"io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem"},{"count":13,"class":"io.quarkus.deployment.builditem.CapabilityBuildItem"},{"count":10,"class":"io.quarkus.deployment.builditem.SuppressNonRuntimeConfigChangedWarningBuildItem"},{"count":10,"class":"io.quarkus.deployment.builditem.FeatureBuildItem"},{"count":10,"class":"io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem"},{"count":9,"class":"io.quarkus.vertx.http.deployment.webjar.WebJarBuildItem"},{"count":9,"class":"io.quarkus.devui.deployment.InternalPageBuildItem"},{"count":9,"class":"io.quarkus.devui.spi.buildtime.BuildTimeActionBuildItem"},{"count":8,"class":"io.quarkus.devui.deployment.DevUIWebJarBuildItem"},{"count":8,"class":"io.quarkus.devui.deployment.DevUIRoutesBuildItem"},{"count":8,"class":"io.quarkus.arc.deployment.AnnotationsTransformerBuildItem"},{"count":8,"class":"io.quarkus.deployment.logging.LogCleanupFilterBuildItem"},{"count":7,"class":"io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem"},{"count":7,"class":"io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem"},{"count":7,"class":"io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem"},{"count":6,"class":"io.quarkus.deployment.builditem.SystemPropertyBuildItem"},{"count":6,"class":"io.quarkus.deployment.builditem.ConsoleCommandBuildItem"},{"count":6,"class":"io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem"},{"count":6,"class":"io.quarkus.devui.spi.page.CardPageBuildItem"},{"count":6,"class":"io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem"},{"count":5,"class":"io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem"},{"count":5,"class":"io.quarkus.arc.deployment.GeneratedBeanBuildItem"},{"count":4,"class":"io.quarkus.jackson.spi.ClassPathJacksonModuleBuildItem"},{"count":4,"class":"io.quarkus.deployment.builditem.nativeimage.ReflectiveFieldBuildItem"},{"count":4,"class":"io.quarkus.resteasy.reactive.spi.MessageBodyWriterOverrideBuildItem"},{"count":4,"class":"io.quarkus.resteasy.reactive.spi.MessageBodyReaderBuildItem"},{"count":4,"class":"io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem"},{"count":4,"class":"io.quarkus.vertx.http.deployment.spi.RouteBuildItem"},{"count":4,"class":"io.quarkus.devui.deployment.BuildTimeConstBuildItem"},{"count":4,"class":"io.quarkus.resteasy.reactive.spi.MessageBodyReaderOverrideBuildItem"},{"count":4,"class":"io.quarkus.deployment.builditem.AdditionalApplicationArchiveMarkerBuildItem"},{"count":3,"class":"io.quarkus.vertx.http.deployment.HttpAuthMechanismAnnotationBuildItem"},{"count":3,"class":"io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem"},{"count":3,"class":"io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem"},{"count":3,"class":"io.quarkus.deployment.builditem.ServiceStartBuildItem"},{"count":3,"class":"io.quarkus.vertx.http.deployment.FilterBuildItem"},{"count":3,"class":"io.quarkus.arc.deployment.AutoAddScopeBuildItem"},{"count":3,"class":"io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem"},{"count":3,"class":"io.quarkus.resteasy.reactive.spi.CustomExceptionMapperBuildItem"},{"count":3,"class":"io.quarkus.deployment.builditem.GeneratedResourceBuildItem"},{"count":2,"class":"io.quarkus.deployment.builditem.ShutdownListenerBuildItem"},{"count":2,"class":"io.quarkus.resteasy.reactive.common.deployment.ResourceInterceptorsContributorBuildItem"},{"count":2,"class":"io.quarkus.deployment.builditem.ObjectSubstitutionBuildItem"},{"count":2,"class":"io.quarkus.devui.spi.buildtime.QuteTemplateBuildItem"},{"count":2,"class":"io.quarkus.deployment.builditem.StaticInitConfigBuilderBuildItem"},{"count":2,"class":"io.quarkus.deployment.builditem.RecordableConstructorBuildItem"},{"count":2,"class":"io.quarkus.webdependency.locator.deployment.devui.WebDependencyLibrariesBuildItem"},{"count":2,"class":"io.quarkus.deployment.builditem.BytecodeRecorderObjectLoaderBuildItem"},{"count":2,"class":"io.quarkus.devui.spi.buildtime.StaticContentBuildItem"},{"count":2,"class":"io.quarkus.deployment.builditem.LogCategoryBuildItem"},{"count":2,"class":"io.quarkus.deployment.dev.testing.TestListenerBuildItem"},{"count":2,"class":"io.quarkus.resteasy.reactive.server.spi.UnwrappedExceptionBuildItem"},{"count":2,"class":"io.quarkus.deployment.builditem.ConfigMappingBuildItem"},{"count":2,"class":"io.quarkus.devui.deployment.InternalImportMapBuildItem"},{"count":2,"class":"io.quarkus.arc.deployment.AutoInjectAnnotationBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.AnnotationProxyBuildItem"},{"count":1,"class":"io.quarkus.devui.deployment.MvnpmBuildItem"},{"count":1,"class":"io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem"},{"count":1,"class":"io.quarkus.deployment.console.ConsoleInstalledBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.SynthesisFinishedBuildItem"},{"count":1,"class":"io.quarkus.vertx.core.deployment.EventLoopCountBuildItem"},{"count":1,"class":"io.quarkus.vertx.core.deployment.CoreVertxBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.server.deployment.ContextResolversBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyIgnoreWarningBuildItem"},{"count":1,"class":"io.quarkus.vertx.deployment.LocalCodecSelectorTypesBuildItem"},{"count":1,"class":"io.quarkus.vertx.http.deployment.InitialRouterBuildItem"},{"count":1,"class":"io.quarkus.smallrye.openapi.deployment.spi.OpenApiDocumentBuildItem"},{"count":1,"class":"io.quarkus.swaggerui.deployment.SwaggerUiBuildItem"},{"count":1,"class":"io.quarkus.deployment.dev.ExceptionNotificationBuildItem"},{"count":1,"class":"io.quarkus.deployment.pkg.builditem.CompiledJavaVersionBuildItem"},{"count":1,"class":"io.quarkus.devui.spi.workspace.WorkspaceBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.ValidationPhaseBuildItem"},{"count":1,"class":"io.quarkus.netty.deployment.EventLoopSupplierBuildItem"},{"count":1,"class":"io.quarkus.deployment.BooleanSupplierFactoryBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.server.deployment.ParamConverterProvidersBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.server.spi.HandlerConfigurationProviderBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.ThreadFactoryBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.ApplicationIndexBuildItem"},{"count":1,"class":"io.quarkus.deployment.logging.LoggingSetupBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.ArcContainerBuildItem"},{"count":1,"class":"io.quarkus.devui.deployment.JsonRPCRuntimeMethodsBuildItem"},{"count":1,"class":"io.quarkus.smallrye.context.deployment.spi.ThreadContextProviderBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.ApplicationClassNameBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.StreamingLogHandlerBuildItem"},{"count":1,"class":"io.quarkus.deployment.dev.DisableInstrumentationForIndexPredicateBuildItem"},{"count":1,"class":"io.quarkus.devui.spi.page.UnlistedPageBuildItem"},{"count":1,"class":"io.quarkus.deployment.logging.LoggingDecorateBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.server.spi.GlobalHandlerCustomizerBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.CurrentContextFactoryBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.common.deployment.ParameterContainersBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.GeneratedRuntimeSystemPropertyBuildItem"},{"count":1,"class":"io.quarkus.smallrye.openapi.deployment.spi.AddToOpenAPIDefinitionBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.ConfigurationBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.common.deployment.ApplicationResultBuildItem"},{"count":1,"class":"io.quarkus.vertx.http.deployment.BodyHandlerBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.BuildExclusionsBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.LogCategoryMinLevelDefaultsBuildItem"},{"count":1,"class":"ai.timefold.solver.quarkus.deployment.SolverConfigBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.IOThreadDetectorBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.InvokerFactoryBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.SslNativeConfigBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.common.deployment.ServerDefaultProducesHandlerBuildItem"},{"count":1,"class":"io.quarkus.deployment.ide.IdeRunningProcessBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.TransformedClassesBuildItem"},{"count":1,"class":"io.quarkus.netty.deployment.EventLoopGroupBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.devui.ArcBeanInfoBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem$BeanConfiguratorBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.RunTimeConfigurationProxyBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.common.deployment.ResourceInterceptorsBuildItem"},{"count":1,"class":"io.quarkus.vertx.http.deployment.DefaultRouteBuildItem"},{"count":1,"class":"io.quarkus.vertx.http.deployment.VertxDevUILogBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.BuildCompatibleExtensionsBuildItem"},{"count":1,"class":"io.quarkus.devui.deployment.ThemeVarsBuildItem"},{"count":1,"class":"io.quarkus.devui.spi.page.FooterPageBuildItem"},{"count":1,"class":"io.quarkus.smallrye.context.deployment.ContextPropagationInitializedBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.server.deployment.ExceptionMappersBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.InterceptorResolverBuildItem"},{"count":1,"class":"io.quarkus.devui.spi.page.SettingPageBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.IndexDependencyBuildItem"},{"count":1,"class":"io.quarkus.devui.spi.workspace.WorkspaceActionBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.BeanArchiveIndexBuildItem"},{"count":1,"class":"io.quarkus.jackson.spi.JacksonModuleBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.ConsoleFormatterBannerBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.SuppressConditionGeneratorBuildItem"},{"count":1,"class":"io.quarkus.tls.deployment.spi.TlsRegistryBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.BuildTimeEnabledStereotypesBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.ApplicationArchivesBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.ContextHandlerBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.TransformedAnnotationsBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveResourceMethodEntriesBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.GeneratedFileSystemResourceHandledBuildItem"},{"count":1,"class":"io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem"},{"count":1,"class":"io.quarkus.vertx.core.deployment.IgnoredContextLocalDataKeysBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.PreBeanContainerBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.InjectionPointTransformerBuildItem"},{"count":1,"class":"io.quarkus.vertx.http.deployment.webjar.WebJarResultsBuildItem"},{"count":1,"class":"io.quarkus.netty.deployment.MinNettyAllocatorMaxOrderBuildItem"},{"count":1,"class":"io.quarkus.smallrye.openapi.deployment.OpenApiFilteredIndexViewBuildItem"},{"count":1,"class":"io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem"},{"count":1,"class":"io.quarkus.vertx.http.deployment.VertxWebRouterBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.CombinedIndexBuildItem"},{"count":1,"class":"io.quarkus.deployment.Capabilities"},{"count":1,"class":"io.quarkus.devui.deployment.ExtensionsBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.ExecutorBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.server.deployment.SetupEndpointsResultBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveDeploymentInfoBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.ObserverRegistrationPhaseBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.common.deployment.ResourceScanningResultBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.server.deployment.ServerSerialisersBuildItem"},{"count":1,"class":"io.quarkus.vertx.deployment.VertxBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveDeploymentBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.BeanContainerBuildItem"},{"count":1,"class":"io.quarkus.devui.spi.buildtime.FooterLogBuildItem"},{"count":1,"class":"io.quarkus.deployment.ide.EffectiveIdeBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.common.deployment.JaxRsResourceIndexBuildItem"},{"count":1,"class":"io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem"},{"count":1,"class":"io.quarkus.vertx.http.deployment.HttpRootPathBuildItem"},{"count":1,"class":"io.quarkus.devui.deployment.DeploymentMethodBuildItem"},{"count":1,"class":"io.quarkus.deployment.steps.CapabilityAggregationStep$CapabilitiesConfiguredInDescriptorsBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.ApplicationStartBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.ConfigPropertyBuildItem"},{"count":1,"class":"io.quarkus.devui.deployment.RelocationImportMapBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.common.deployment.AggregatedParameterContainersBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.ContextRegistrationPhaseBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem"},{"count":1,"class":"io.quarkus.resteasy.reactive.server.deployment.BuiltInReaderOverrideBuildItem"},{"count":1,"class":"io.quarkus.arc.deployment.CompletedApplicationClassPredicateBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.ApplicationInfoBuildItem"},{"count":1,"class":"io.quarkus.deployment.ide.IdeFileBuildItem"},{"count":1,"class":"io.quarkus.deployment.builditem.MainClassBuildItem"}],"itemsCount":3198,"buildTarget":"employee-scheduling-1.0-SNAPSHOT"} \ No newline at end of file diff --git a/target/classes/META-INF/resources/app.js b/target/classes/META-INF/resources/app.js new file mode 100644 index 0000000..eca3308 --- /dev/null +++ b/target/classes/META-INF/resources/app.js @@ -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($('' + item + '')); + $("#" + 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("

No test data available

") + }); +} + +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 = $('
') + .append($(`
)`) + .append(employee.name)) + .append($('
') + .append($(employee.skills.map(skill => `${skill}`).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 = $(`
`) + .append($(`
`).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 = $(`
`) + .append($(`
`).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 = $(`
`) + .append($(`
`).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 = $('
') + .append($(`
)`) + .append("Unassigned")) + .append($('
') + .append($(`${shift.requiredSkill}`))); + + 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 = $('
') + .append($(`
)`) + .append(shift.location)) + .append($('
') + .append($(`${shift.requiredSkill}`))); + const byLocationShiftElement = $('
') + .append($(`
)`) + .append(shift.employee.name)) + .append($('
') + .append($(`${shift.requiredSkill}`))); + + 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($(`

`).text(`There are no unassigned shifts.`)); + } else { + unassignedShifts.append($(`

`).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 = $(``).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(/(-?\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( + $(`
+ +
`)); + } + + const timefoldFooter = $("footer#timefold-auto-footer"); + if (timefoldFooter != null) { + timefoldFooter.append( + $(``)); + } +} diff --git a/target/classes/META-INF/resources/index.html b/target/classes/META-INF/resources/index.html new file mode 100644 index 0000000..7c1b361 --- /dev/null +++ b/target/classes/META-INF/resources/index.html @@ -0,0 +1,161 @@ + + + + + Employee scheduling - Timefold Solver on Quarkus + + + + + + + + + + +
+ +
+
+
+
+
+
+

Employee scheduling solver

+

Generate the optimal schedule for your employees.

+ +
+ + + + Score: ? + + +
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+

REST API Guide

+ +

Employee Scheduling solver integration via cURL

+ +

1. Download demo data

+
+            
+            curl -X GET -H 'Accept:application/json' http://localhost:8080/demo-data/SMALL -o sample.json
+    
+ +

2. Post the sample data for solving

+

The POST operation returns a jobId that should be used in subsequent commands.

+
+            
+            curl -X POST -H 'Content-Type:application/json' http://localhost:8080/schedules -d@sample.json
+    
+ +

3. Get the current status and score

+
+            
+            curl -X GET -H 'Accept:application/json' http://localhost:8080/schedules/{jobId}/status
+    
+ +

4. Get the complete solution

+
+            
+            curl -X GET -H 'Accept:application/json' http://localhost:8080/schedules/{jobId}
+    
+ +

5. Fetch the analysis of the solution

+
+            
+            curl -X PUT -H 'Content-Type:application/json' http://localhost:8080/schedules/analyze -d@solution.json
+    
+ +

5. Terminate solving early

+
+            
+            curl -X DELETE -H 'Accept:application/json' http://localhost:8080/schedules/{id}
+    
+
+ +
+

REST API Reference

+
+ + +
+
+
+
+ + + + + + + + + + diff --git a/target/classes/application.properties b/target/classes/application.properties new file mode 100644 index 0000000..2e80d17 --- /dev/null +++ b/target/classes/application.properties @@ -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 diff --git a/target/classes/org/acme/employeescheduling/domain/Employee.class b/target/classes/org/acme/employeescheduling/domain/Employee.class new file mode 100644 index 0000000..070353f Binary files /dev/null and b/target/classes/org/acme/employeescheduling/domain/Employee.class differ diff --git a/target/classes/org/acme/employeescheduling/domain/EmployeeSchedule.class b/target/classes/org/acme/employeescheduling/domain/EmployeeSchedule.class new file mode 100644 index 0000000..aacd7c2 Binary files /dev/null and b/target/classes/org/acme/employeescheduling/domain/EmployeeSchedule.class differ diff --git a/target/classes/org/acme/employeescheduling/domain/Shift.class b/target/classes/org/acme/employeescheduling/domain/Shift.class new file mode 100644 index 0000000..fe81e61 Binary files /dev/null and b/target/classes/org/acme/employeescheduling/domain/Shift.class differ diff --git a/target/classes/org/acme/employeescheduling/rest/DemoDataGenerator$CountDistribution.class b/target/classes/org/acme/employeescheduling/rest/DemoDataGenerator$CountDistribution.class new file mode 100644 index 0000000..691b0fd Binary files /dev/null and b/target/classes/org/acme/employeescheduling/rest/DemoDataGenerator$CountDistribution.class differ diff --git a/target/classes/org/acme/employeescheduling/rest/DemoDataGenerator$DemoData.class b/target/classes/org/acme/employeescheduling/rest/DemoDataGenerator$DemoData.class new file mode 100644 index 0000000..aca24c4 Binary files /dev/null and b/target/classes/org/acme/employeescheduling/rest/DemoDataGenerator$DemoData.class differ diff --git a/target/classes/org/acme/employeescheduling/rest/DemoDataGenerator$DemoDataParameters.class b/target/classes/org/acme/employeescheduling/rest/DemoDataGenerator$DemoDataParameters.class new file mode 100644 index 0000000..ac72abc Binary files /dev/null and b/target/classes/org/acme/employeescheduling/rest/DemoDataGenerator$DemoDataParameters.class differ diff --git a/target/classes/org/acme/employeescheduling/rest/DemoDataGenerator.class b/target/classes/org/acme/employeescheduling/rest/DemoDataGenerator.class new file mode 100644 index 0000000..79a67f1 Binary files /dev/null and b/target/classes/org/acme/employeescheduling/rest/DemoDataGenerator.class differ diff --git a/target/classes/org/acme/employeescheduling/rest/EmployeeScheduleDemoResource.class b/target/classes/org/acme/employeescheduling/rest/EmployeeScheduleDemoResource.class new file mode 100644 index 0000000..8691997 Binary files /dev/null and b/target/classes/org/acme/employeescheduling/rest/EmployeeScheduleDemoResource.class differ diff --git a/target/classes/org/acme/employeescheduling/rest/EmployeeScheduleResource$Job.class b/target/classes/org/acme/employeescheduling/rest/EmployeeScheduleResource$Job.class new file mode 100644 index 0000000..cfa9eca Binary files /dev/null and b/target/classes/org/acme/employeescheduling/rest/EmployeeScheduleResource$Job.class differ diff --git a/target/classes/org/acme/employeescheduling/rest/EmployeeScheduleResource.class b/target/classes/org/acme/employeescheduling/rest/EmployeeScheduleResource.class new file mode 100644 index 0000000..0ccf75a Binary files /dev/null and b/target/classes/org/acme/employeescheduling/rest/EmployeeScheduleResource.class differ diff --git a/target/classes/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverException.class b/target/classes/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverException.class new file mode 100644 index 0000000..de97bf5 Binary files /dev/null and b/target/classes/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverException.class differ diff --git a/target/classes/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverExceptionMapper.class b/target/classes/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverExceptionMapper.class new file mode 100644 index 0000000..48aa4e5 Binary files /dev/null and b/target/classes/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverExceptionMapper.class differ diff --git a/target/classes/org/acme/employeescheduling/rest/exception/ErrorInfo.class b/target/classes/org/acme/employeescheduling/rest/exception/ErrorInfo.class new file mode 100644 index 0000000..520d44d Binary files /dev/null and b/target/classes/org/acme/employeescheduling/rest/exception/ErrorInfo.class differ diff --git a/target/classes/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProvider.class b/target/classes/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProvider.class new file mode 100644 index 0000000..81a4498 Binary files /dev/null and b/target/classes/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProvider.class differ diff --git a/target/employee-scheduling-dev.jar b/target/employee-scheduling-dev.jar new file mode 100644 index 0000000..628c3e4 Binary files /dev/null and b/target/employee-scheduling-dev.jar differ diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..04ac5e6 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,14 @@ +org/acme/employeescheduling/domain/EmployeeSchedule.class +org/acme/employeescheduling/rest/DemoDataGenerator$CountDistribution.class +org/acme/employeescheduling/rest/DemoDataGenerator$DemoDataParameters.class +org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverException.class +org/acme/employeescheduling/rest/EmployeeScheduleDemoResource.class +org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProvider.class +org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverExceptionMapper.class +org/acme/employeescheduling/rest/EmployeeScheduleResource$Job.class +org/acme/employeescheduling/rest/EmployeeScheduleResource.class +org/acme/employeescheduling/rest/DemoDataGenerator.class +org/acme/employeescheduling/domain/Shift.class +org/acme/employeescheduling/domain/Employee.class +org/acme/employeescheduling/rest/exception/ErrorInfo.class +org/acme/employeescheduling/rest/DemoDataGenerator$DemoData.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..df48964 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,10 @@ +/home/virt/timefold-quickstarts/java/employee-scheduling/src/main/java/org/acme/employeescheduling/domain/Employee.java +/home/virt/timefold-quickstarts/java/employee-scheduling/src/main/java/org/acme/employeescheduling/domain/EmployeeSchedule.java +/home/virt/timefold-quickstarts/java/employee-scheduling/src/main/java/org/acme/employeescheduling/domain/Shift.java +/home/virt/timefold-quickstarts/java/employee-scheduling/src/main/java/org/acme/employeescheduling/rest/DemoDataGenerator.java +/home/virt/timefold-quickstarts/java/employee-scheduling/src/main/java/org/acme/employeescheduling/rest/EmployeeScheduleDemoResource.java +/home/virt/timefold-quickstarts/java/employee-scheduling/src/main/java/org/acme/employeescheduling/rest/EmployeeScheduleResource.java +/home/virt/timefold-quickstarts/java/employee-scheduling/src/main/java/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverException.java +/home/virt/timefold-quickstarts/java/employee-scheduling/src/main/java/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverExceptionMapper.java +/home/virt/timefold-quickstarts/java/employee-scheduling/src/main/java/org/acme/employeescheduling/rest/exception/ErrorInfo.java +/home/virt/timefold-quickstarts/java/employee-scheduling/src/main/java/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProvider.java diff --git a/target/maven-status/maven-compiler-plugin/compile/null/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/null/createdFiles.lst new file mode 100644 index 0000000..e69de29 diff --git a/target/maven-status/maven-compiler-plugin/compile/null/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/null/inputFiles.lst new file mode 100644 index 0000000..55f3d04 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/null/inputFiles.lst @@ -0,0 +1,10 @@ +/home/virt/timefold-quickstarts/java/collect-sang/src/main/java/org/acme/employeescheduling/domain/Employee.java +/home/virt/timefold-quickstarts/java/collect-sang/src/main/java/org/acme/employeescheduling/domain/EmployeeSchedule.java +/home/virt/timefold-quickstarts/java/collect-sang/src/main/java/org/acme/employeescheduling/domain/Shift.java +/home/virt/timefold-quickstarts/java/collect-sang/src/main/java/org/acme/employeescheduling/rest/DemoDataGenerator.java +/home/virt/timefold-quickstarts/java/collect-sang/src/main/java/org/acme/employeescheduling/rest/EmployeeScheduleDemoResource.java +/home/virt/timefold-quickstarts/java/collect-sang/src/main/java/org/acme/employeescheduling/rest/EmployeeScheduleResource.java +/home/virt/timefold-quickstarts/java/collect-sang/src/main/java/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverException.java +/home/virt/timefold-quickstarts/java/collect-sang/src/main/java/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverExceptionMapper.java +/home/virt/timefold-quickstarts/java/collect-sang/src/main/java/org/acme/employeescheduling/rest/exception/ErrorInfo.java +/home/virt/timefold-quickstarts/java/collect-sang/src/main/java/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProvider.java diff --git a/target/maven-status/maven-compiler-plugin/testCompile/null/createdFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/null/createdFiles.lst new file mode 100644 index 0000000..7d6a317 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/testCompile/null/createdFiles.lst @@ -0,0 +1,4 @@ +org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProviderTest.class +org/acme/employeescheduling/rest/EmployeeScheduleResourceTest.class +org/acme/employeescheduling/rest/EmployeeSchedulingEnvironmentTest.class +org/acme/employeescheduling/rest/EmployeeSchedulingResourceIT.class diff --git a/target/maven-status/maven-compiler-plugin/testCompile/null/inputFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/null/inputFiles.lst new file mode 100644 index 0000000..c6fe8c2 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/testCompile/null/inputFiles.lst @@ -0,0 +1,4 @@ +/home/virt/timefold-quickstarts/java/employee-scheduling/src/test/java/org/acme/employeescheduling/rest/EmployeeScheduleResourceTest.java +/home/virt/timefold-quickstarts/java/employee-scheduling/src/test/java/org/acme/employeescheduling/rest/EmployeeSchedulingEnvironmentTest.java +/home/virt/timefold-quickstarts/java/employee-scheduling/src/test/java/org/acme/employeescheduling/rest/EmployeeSchedulingResourceIT.java +/home/virt/timefold-quickstarts/java/employee-scheduling/src/test/java/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProviderTest.java diff --git a/target/quarkus/bootstrap/dev-app-model.dat b/target/quarkus/bootstrap/dev-app-model.dat new file mode 100644 index 0000000..ff441c9 Binary files /dev/null and b/target/quarkus/bootstrap/dev-app-model.dat differ diff --git a/target/test-classes/org/acme/employeescheduling/rest/EmployeeScheduleResourceTest.class b/target/test-classes/org/acme/employeescheduling/rest/EmployeeScheduleResourceTest.class new file mode 100644 index 0000000..8df3bc6 Binary files /dev/null and b/target/test-classes/org/acme/employeescheduling/rest/EmployeeScheduleResourceTest.class differ diff --git a/target/test-classes/org/acme/employeescheduling/rest/EmployeeSchedulingEnvironmentTest.class b/target/test-classes/org/acme/employeescheduling/rest/EmployeeSchedulingEnvironmentTest.class new file mode 100644 index 0000000..33abebd Binary files /dev/null and b/target/test-classes/org/acme/employeescheduling/rest/EmployeeSchedulingEnvironmentTest.class differ diff --git a/target/test-classes/org/acme/employeescheduling/rest/EmployeeSchedulingResourceIT.class b/target/test-classes/org/acme/employeescheduling/rest/EmployeeSchedulingResourceIT.class new file mode 100644 index 0000000..c942089 Binary files /dev/null and b/target/test-classes/org/acme/employeescheduling/rest/EmployeeSchedulingResourceIT.class differ diff --git a/target/test-classes/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProviderTest.class b/target/test-classes/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProviderTest.class new file mode 100644 index 0000000..6338494 Binary files /dev/null and b/target/test-classes/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProviderTest.class differ diff --git a/vi b/vi new file mode 100644 index 0000000..18e541f --- /dev/null +++ b/vi @@ -0,0 +1,162 @@ +{ + "employees": [ + { + "name": "Marie Dupont", + "skills": ["INFIRMIER", "PRELEVEMENT"], + "unavailableDates": ["2024-12-25", "2024-12-26"], + "undesiredDates": ["2024-12-24", "2024-12-31"], + "desiredDates": ["2024-12-20", "2024-12-21"] + }, + { + "name": "Pierre Martin", + "skills": ["MEDECIN", "SUPERVISION"], + "unavailableDates": ["2024-12-30"], + "undesiredDates": ["2024-12-29"], + "desiredDates": ["2024-12-22", "2024-12-23"] + }, + { + "name": "Sophie Bernard", + "skills": ["INFIRMIER", "ACCUEIL"], + "unavailableDates": [], + "undesiredDates": ["2024-12-25"], + "desiredDates": ["2024-12-24", "2024-12-28"] + }, + { + "name": "Jean Leroy", + "skills": ["PRELEVEMENT", "TRANSPORT"], + "unavailableDates": ["2024-12-24", "2024-12-25"], + "undesiredDates": [], + "desiredDates": ["2024-12-27", "2024-12-30"] + }, + { + "name": "Anne Moreau", + "skills": ["MEDECIN", "INFIRMIER"], + "unavailableDates": ["2024-12-26"], + "undesiredDates": ["2024-12-31"], + "desiredDates": ["2024-12-21", "2024-12-29"] + }, + { + "name": "Luc Petit", + "skills": ["ACCUEIL", "TRANSPORT"], + "unavailableDates": [], + "undesiredDates": ["2024-12-20"], + "desiredDates": ["2024-12-25", "2024-12-26"] + } + ], + "shifts": [ + { + "id": "shift_001", + "start": "2024-12-20T08:00:00", + "end": "2024-12-20T16:00:00", + "location": "Centre de collecte - Toulouse", + "requiredSkill": "INFIRMIER", + "employee": null + }, + { + "id": "shift_002", + "start": "2024-12-20T14:00:00", + "end": "2024-12-20T22:00:00", + "location": "Centre de collecte - Toulouse", + "requiredSkill": "PRELEVEMENT", + "employee": null + }, + { + "id": "shift_003", + "start": "2024-12-21T06:00:00", + "end": "2024-12-21T14:00:00", + "location": "Hôpital Purpan", + "requiredSkill": "MEDECIN", + "employee": null + }, + { + "id": "shift_004", + "start": "2024-12-21T08:00:00", + "end": "2024-12-21T12:00:00", + "location": "Centre de collecte - Colomiers", + "requiredSkill": "ACCUEIL", + "employee": null + }, + { + "id": "shift_005", + "start": "2024-12-22T09:00:00", + "end": "2024-12-22T17:00:00", + "location": "Centre de collecte - Blagnac", + "requiredSkill": "INFIRMIER", + "employee": null + }, + { + "id": "shift_006", + "start": "2024-12-22T13:00:00", + "end": "2024-12-22T18:00:00", + "location": "Transport mobile", + "requiredSkill": "TRANSPORT", + "employee": null + }, + { + "id": "shift_007", + "start": "2024-12-23T07:00:00", + "end": "2024-12-23T15:00:00", + "location": "Hôpital Rangueil", + "requiredSkill": "MEDECIN", + "employee": null + }, + { + "id": "shift_008", + "start": "2024-12-23T10:00:00", + "end": "2024-12-23T16:00:00", + "location": "Centre de collecte - Toulouse", + "requiredSkill": "PRELEVEMENT", + "employee": null + }, + { + "id": "shift_009", + "start": "2024-12-24T08:00:00", + "end": "2024-12-24T14:00:00", + "location": "Centre de collecte - Colomiers", + "requiredSkill": "INFIRMIER", + "employee": null + }, + { + "id": "shift_010", + "start": "2024-12-27T09:00:00", + "end": "2024-12-27T17:00:00", + "location": "Centre de collecte - Toulouse", + "requiredSkill": "SUPERVISION", + "employee": null + }, + { + "id": "shift_011", + "start": "2024-12-28T08:00:00", + "end": "2024-12-28T16:00:00", + "location": "Centre de collecte - Blagnac", + "requiredSkill": "ACCUEIL", + "employee": null + }, + { + "id": "shift_012", + "start": "2024-12-29T06:00:00", + "end": "2024-12-29T14:00:00", + "location": "Hôpital Purpan", + "requiredSkill": "MEDECIN", + "employee": null + }, + { + "id": "shift_013", + "start": "2024-12-30T10:00:00", + "end": "2024-12-30T18:00:00", + "location": "Transport mobile", + "requiredSkill": "TRANSPORT", + "employee": null + }, + { + "id": "shift_014", + "start": "2024-12-31T08:00:00", + "end": "2024-12-31T16:00:00", + "location": "Centre de collecte - Toulouse", + "requiredSkill": "INFIRMIER", + "employee": null + } + ], + "score": null, + "solverStatus": null +}