浏览代码

implement interaction with optaplanner

tripeur 4 年之前
父节点
当前提交
4b5a315861

+ 143 - 27
package-lock.json

@@ -166,6 +166,12 @@
         "fastq": "^1.6.0"
       }
     },
+    "@polka/url": {
+      "version": "1.0.0-next.12",
+      "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.12.tgz",
+      "integrity": "sha512-6RglhutqrGFMO1MNUXp95RBuYIuc8wTnMAV5MUhLmjTOy78ncwOw7RgeQ/HeymkKXRhZd0s2DNrM1rL7unk3MQ==",
+      "dev": true
+    },
     "@soda/friendly-errors-webpack-plugin": {
       "version": "1.8.0",
       "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz",
@@ -1048,6 +1054,16 @@
             "universalify": "^0.1.0"
           }
         },
+        "gzip-size": {
+          "version": "5.1.1",
+          "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
+          "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==",
+          "dev": true,
+          "requires": {
+            "duplexer": "^0.1.1",
+            "pify": "^4.0.1"
+          }
+        },
         "jsonfile": {
           "version": "4.0.0",
           "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
@@ -1154,6 +1170,27 @@
           "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
           "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
           "dev": true
+        },
+        "webpack-bundle-analyzer": {
+          "version": "3.9.0",
+          "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.0.tgz",
+          "integrity": "sha512-Ob8amZfCm3rMB1ScjQVlbYYUEJyEjdEtQ92jqiFUYt5VkEeO2v5UMbv49P/gnmCZm3A6yaFQzCBvpZqN4MUsdA==",
+          "dev": true,
+          "requires": {
+            "acorn": "^7.1.1",
+            "acorn-walk": "^7.1.1",
+            "bfj": "^6.1.1",
+            "chalk": "^2.4.1",
+            "commander": "^2.18.0",
+            "ejs": "^2.6.1",
+            "express": "^4.16.3",
+            "filesize": "^3.6.1",
+            "gzip-size": "^5.0.0",
+            "lodash": "^4.17.19",
+            "mkdirp": "^0.5.1",
+            "opener": "^1.5.1",
+            "ws": "^6.0.0"
+          }
         }
       }
     },
@@ -5757,13 +5794,12 @@
       "dev": true
     },
     "gzip-size": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
-      "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==",
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
+      "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
       "dev": true,
       "requires": {
-        "duplexer": "^0.1.1",
-        "pify": "^4.0.1"
+        "duplexer": "^0.1.2"
       }
     },
     "handle-thing": {
@@ -6909,12 +6945,11 @@
       "dev": true
     },
     "jc-timeline": {
-      "version": "git+http://gitlab.jaquin.fr/clovis/jc-timeline.git#e24f7f59c7bfaebfa3a1eb10b78052d9eab118aa",
+      "version": "git+http://gitlab.jaquin.fr/clovis/jc-timeline.git#28da6f3ee43b04c67ffbae54b0cbb7f12675a662",
       "from": "git+http://gitlab.jaquin.fr/clovis/jc-timeline.git",
       "requires": {
         "dayjs": "^1.10.4",
         "lit-element": "^2.4.0",
-        "prettier": "^2.2.1",
         "simplebar": "^5.3.0"
       }
     },
@@ -10208,7 +10243,8 @@
     "prettier": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz",
-      "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q=="
+      "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==",
+      "dev": true
     },
     "prettier-linter-helpers": {
       "version": "1.0.0",
@@ -11301,6 +11337,17 @@
         "resize-observer-polyfill": "^1.5.1"
       }
     },
+    "sirv": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.11.tgz",
+      "integrity": "sha512-SR36i3/LSWja7AJNRBz4fF/Xjpn7lQFI30tZ434dIy+bitLYSP+ZEenHg36i23V2SGEz+kqjksg0uOGZ5LPiqg==",
+      "dev": true,
+      "requires": {
+        "@polka/url": "^1.0.0-next.9",
+        "mime": "^2.3.1",
+        "totalist": "^1.0.0"
+      }
+    },
     "slash": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
@@ -12282,6 +12329,12 @@
       "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=",
       "dev": true
     },
+    "totalist": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz",
+      "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==",
+      "dev": true
+    },
     "tough-cookie": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
@@ -13183,30 +13236,93 @@
       }
     },
     "webpack-bundle-analyzer": {
-      "version": "3.9.0",
-      "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.0.tgz",
-      "integrity": "sha512-Ob8amZfCm3rMB1ScjQVlbYYUEJyEjdEtQ92jqiFUYt5VkEeO2v5UMbv49P/gnmCZm3A6yaFQzCBvpZqN4MUsdA==",
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.1.tgz",
+      "integrity": "sha512-j5m7WgytCkiVBoOGavzNokBOqxe6Mma13X1asfVYtKWM3wxBiRRu1u1iG0Iol5+qp9WgyhkMmBAcvjEfJ2bdDw==",
       "dev": true,
       "requires": {
-        "acorn": "^7.1.1",
-        "acorn-walk": "^7.1.1",
-        "bfj": "^6.1.1",
-        "chalk": "^2.4.1",
-        "commander": "^2.18.0",
-        "ejs": "^2.6.1",
-        "express": "^4.16.3",
-        "filesize": "^3.6.1",
-        "gzip-size": "^5.0.0",
-        "lodash": "^4.17.19",
-        "mkdirp": "^0.5.1",
-        "opener": "^1.5.1",
-        "ws": "^6.0.0"
+        "acorn": "^8.0.4",
+        "acorn-walk": "^8.0.0",
+        "chalk": "^4.1.0",
+        "commander": "^6.2.0",
+        "gzip-size": "^6.0.0",
+        "lodash": "^4.17.20",
+        "opener": "^1.5.2",
+        "sirv": "^1.0.7",
+        "ws": "^7.3.1"
       },
       "dependencies": {
         "acorn": {
-          "version": "7.4.1",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
-          "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+          "version": "8.2.4",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.2.4.tgz",
+          "integrity": "sha512-Ibt84YwBDDA890eDiDCEqcbwvHlBvzzDkU2cGBBDDI1QWT12jTiXIOn2CIw5KK4i6N5Z2HUxwYjzriDyqaqqZg==",
+          "dev": true
+        },
+        "acorn-walk": {
+          "version": "8.1.0",
+          "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.1.0.tgz",
+          "integrity": "sha512-mjmzmv12YIG/G8JQdQuz2MUDShEJ6teYpT5bmWA4q7iwoGen8xtt3twF3OvzIUl+Q06aWIjvnwQUKvQ6TtMRjg==",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
+          "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "commander": {
+          "version": "6.2.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+          "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
+          "dev": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "ws": {
+          "version": "7.4.5",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
+          "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==",
           "dev": true
         }
       }

+ 5 - 2
package.json

@@ -5,12 +5,14 @@
   "scripts": {
     "serve": "vue-cli-service serve",
     "build": "vue-cli-service build",
-    "lint": "vue-cli-service lint"
+    "lint": "vue-cli-service lint",
+    "deploy":"Copy-Item -Path C:\\Users\\Clovis\\Desktop\\code\\javascript\\bdlg-scheduler\\dist\\* -Destination C:\\Users\\Clovis\\Desktop\\code\\java\\bdlg.planner\\src\\main\\resources\\static -PassThru -Force"
   },
   "dependencies": {
     "dayjs": "^1.10.4",
     "fuse.js": "^6.4.6",
     "jc-timeline": "git+http://gitlab.jaquin.fr/clovis/jc-timeline.git",
+    "lodash": "^4.17.21",
     "postcss": "^8.2.13",
     "uuid": "^8.3.2",
     "vue": "^3.0.0",
@@ -39,7 +41,8 @@
     "sass": "^1.32.8",
     "sass-loader": "^10.1.1",
     "tslib": "^2.2.0",
-    "typescript": "~4.1.5"
+    "typescript": "~4.1.5",
+    "webpack-bundle-analyzer": "^4.4.1"
   },
   "eslintConfig": {
     "root": true,

二进制
public/favicon.ico


+ 106 - 5
src/App.vue

@@ -21,7 +21,15 @@
       @export="exportStateToJson"
       @import="(e) => importJsonState(e, false)"
       @localSave="localSave"
+      @solve="solve"
     />
+    <div v-if="optimisationInProgress" class="modal">
+      <div class="modal-content">
+        <h3>Optimisation en cours</h3>
+        <p>L'ordinateur calcul un meilleur planning</p>
+        <div class="spinner"><span class="material-icons"> sync </span></div>
+      </div>
+    </div>
   </div>
 </template>
 <script lang="ts">
@@ -32,16 +40,17 @@ import Evenement from "./models/Evenement";
 import Benevole from "./models/Benevole";
 import Creneau from "./models/Creneau";
 import Competence from "./models/Competence";
+import { SolverInput, SolverOutput } from "./models/SolverInput";
 import Ressource, { IRessource, RessourceJSON } from "jc-timeline/lib/Ressource";
 import { MutationTypes } from "./store/Mutations";
 import dayjs from "dayjs";
 import { StateJSON } from "./store/State";
-
 const keyofEvent: Array<keyof Evenement> = ["name", "uuid", "start", "end"];
 export default defineComponent({
+  data() {
+    return { optimisationInProgress: false };
+  },
   mounted() {
-    const a = () => toast({ html: "test", inDuration: 500, outDuration: 500, displayLength: 2000 });
-    a();
     const previousState = window.localStorage.getItem("activeState");
     if (previousState) {
       this.importJsonState(JSON.parse(previousState), false);
@@ -65,6 +74,61 @@ export default defineComponent({
       dummy.click();
       setTimeout(() => document.body.removeChild(dummy), 10000);
     },
+    solve() {
+      const meals = this.$store.state.creneauList.filter((o) => o.isMeal);
+      const slots = this.$store.state.creneauList.filter((o) => !o.isMeal);
+      const timeslots = slots.map((o) => o.toTimeslotRaw());
+      const mealSlots = meals.map((o) => o.toMealSlot());
+      const volonteerList = this.$store.state.benevoleList.map((b) => b.toVolonteeRaw());
+      const constraints = this.$store.state.competenceList.map((c) => c.toSkill());
+      const getAssignementPair = (c: Creneau) =>
+        c.benevoleIdList.map((id) => {
+          return { volonteerId: id, slotId: c.id };
+        });
+      const assignements = slots.flatMap(getAssignementPair);
+      const mealAssignements = meals.flatMap(getAssignementPair);
+
+      const body: SolverInput = {
+        constraints,
+        timeslots,
+        mealSlots,
+        volonteerList,
+        assignements,
+        mealAssignements,
+      };
+      const options = {
+        method: "POST",
+        body: JSON.stringify(body),
+        headers: { "Content-type": "application/json; charset=UTF-8" },
+      };
+      this.optimisationInProgress = true;
+      fetch(`http://localhost:8080/planning/solve`, options)
+        .then((response) => response.json())
+        .then(this.updatePlanningWithNewPairing)
+        .catch((error) => {
+          toast({ html: "Pas de réponse du serveur", classes: "error" });
+          this.optimisationInProgress = false;
+        });
+    },
+    updatePlanningWithNewPairing(data: SolverOutput) {
+      this.optimisationInProgress = false;
+      // Display message from the solver
+      data.message
+        .split("\n")
+        .filter((s) => !s)
+        .forEach((s) => toast({ html: s, classes: "error" }));
+      toast({ html: "Le planning a été mis à jour", classes: "error" });
+      console.log("Solver Score :", data.score);
+      // Remove previous timeslot assignement
+      this.$store.commit(MutationTypes.clearBenevole2Creneau, undefined);
+      // Add new timeslot assignement
+      for (const pair of data.assignements) {
+        this.$store.commit(MutationTypes.addBenevole2Creneau, {
+          creneauId: pair.slotId,
+          benevoleId: pair.volonteerId,
+        });
+      }
+    },
     importJsonState(obj: StateJSON, preserve = false) {
       console.log(obj);
       // Remove previous content and load the main event title
@@ -72,7 +136,10 @@ export default defineComponent({
         this.$store.commit(MutationTypes.resetState, undefined);
         const e = Evenement.fromJSON(obj.evenement);
         for (const k of keyofEvent) {
-          this.$store.commit(MutationTypes.editEvenement, { field: k, value: e[k] });
+          this.$store.commit(MutationTypes.editEvenement, {
+            field: k,
+            value: e[k],
+          });
         }
       }
       // Import constraint
@@ -127,4 +194,38 @@ export default defineComponent({
 });
 </script>
 
-<style></style>
+<style>
+.modal {
+  display: flex;
+  justify-content: center;
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  z-index: 100;
+}
+.modal-content {
+  background: white;
+  padding: 16px 32px;
+  margin-top: 100px;
+  height: min-content;
+}
+.spinner {
+  color: var(--color-accent-400);
+  text-align: center;
+}
+.spinner > .material-icons {
+  font-size: 3rem;
+  animation: spin 2s linear infinite;
+}
+@keyframes spin {
+  0% {
+    transform: rotateZ(360deg);
+  }
+  100% {
+    transform: rotateZ(0deg);
+  }
+}
+</style>

二进制
src/assets/logo.png


+ 16 - 2
src/components/EditeurCreneau.vue

@@ -114,6 +114,12 @@
         :modelValue="creneau.penibility"
         @input="inputListener($event, 'penibility')"
       />
+      <checkbox
+        class="s6"
+        label="Créneau repas"
+        :modelValue="creneau.isMeal"
+        @input="checkboxListener($event, 'isMeal')"
+      />
     </form>
   </div>
 </template>
@@ -124,6 +130,7 @@ import Creneau from "@/models/Creneau";
 import AutocompleteOptions from "@/models/AutocompleteOptions";
 import styledInput from "./input.vue";
 import chipsInput from "../components/SelectChipInput.vue";
+import checkbox from "./checkBox.vue";
 import dayjs from "dayjs";
 
 import "@/assets/css/editor-panel.css";
@@ -132,7 +139,7 @@ import { MutationTypes } from "@/store/Mutations";
 // TODO Understand why the component only syncro after a first edition of the date
 export default defineComponent({
   name: "EditeurCreneau",
-  components: { "chips-input": chipsInput, styledInput },
+  components: { "chips-input": chipsInput, styledInput, checkbox },
   props: {
     creneau: {
       type: Object as PropType<Creneau>,
@@ -269,6 +276,10 @@ export default defineComponent({
     inputListener(event: any, field: keyof Creneau) {
       this.updateCreneau(field, event.target.value);
     },
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    checkboxListener(event: any, field: keyof Creneau) {
+      this.updateCreneau(field, event.target.checked);
+    },
     updateForm: function () {
       if (this.creneau) {
         const startDate = dayjs(this.creneau.start);
@@ -291,7 +302,10 @@ export default defineComponent({
   },
   mounted() {
     this.updateForm();
-    if (this.creneau) this.benevoleStrIdList = this.creneau.benevoleIdList.map((s) => s.toString());
+    if (this.creneau) {
+      this.benevoleStrIdList = this.creneau.benevoleIdList.map((s) => s.toString());
+      this.competencesStrIdList = this.creneau.competencesIdList.map((s) => s.toString());
+    }
   },
 });
 </script>

+ 2 - 2
src/components/SelectChipInput.vue

@@ -97,10 +97,10 @@ export default defineComponent({
   emit: ["update:modelValue"],
   methods: {
     toggle(idx: number, e: MouseEvent): void {
+      e.stopPropagation();
       this.items[idx].isChecked = !this.items[idx].isChecked;
       this.emitValue();
-      this.input.focus();
-      e.stopPropagation();
+      setTimeout(this.input.focus, 100);
     },
     emitValue() {
       const values = this.items.filter((o) => o.isChecked).map((o) => o.value);

+ 9 - 1
src/models/Benevole.ts

@@ -1,3 +1,5 @@
+import { VolonteerRaw } from "./SolverInput";
+
 interface IBenevole {
   id?: number;
   name: string;
@@ -59,7 +61,7 @@ export default class Benevole {
       this.surname.charAt(0).toUpperCase()
     );
   }
-  static fromObject(obj: IBenevole) {
+  static fromObject(obj: IBenevole): Benevole {
     let id: number;
     if (obj.id && !isNaN(obj.id)) {
       id = obj.id;
@@ -97,6 +99,12 @@ export default class Benevole {
     delete obj.creneauIdList;
     return obj;
   }
+  toVolonteeRaw(): VolonteerRaw {
+    return {
+      id: this.id,
+      skillsId: this.competenceIdList,
+    };
+  }
   static fromJSON(input: string | BenevoleJSON): Benevole {
     let obj: BenevoleJSON;
     if (typeof input == "string") {

+ 11 - 1
src/models/Competence.ts

@@ -1,3 +1,5 @@
+import { Skill } from "./SolverInput";
+
 export interface ICompetence {
   id?: number;
   name: string;
@@ -44,7 +46,7 @@ export default class Competence {
     this.isPreference = isPreference;
     this.isTeachable = isTeachable;
   }
-  static getBox(v: boolean) {
+  static getBox(v: boolean): string {
     return `<i class="material-icons">${v ? "check_box" : "check_box_outline_blank"}</i>`;
   }
   get overflowDescription(): string {
@@ -82,6 +84,14 @@ export default class Competence {
       isTeachable: this.isTeachable,
     };
   }
+  toSkill(): Skill {
+    return {
+      id: this.id,
+      name: this.name,
+      isPreference: this.isPreference,
+      isTeachable: this.isTeachable,
+    };
+  }
   toJSON(): ICompetence {
     return this.toPlainObject();
   }

+ 26 - 4
src/models/Creneau.ts

@@ -1,6 +1,6 @@
 import dayjs from "dayjs";
-import { isString, omit } from "lodash";
 import { Event, EventJSON } from "jc-timeline";
+import { MealSlot, TimeslotRaw } from "./SolverInput";
 
 export interface ICreneau {
   event: Event;
@@ -10,6 +10,7 @@ export interface ICreneau {
   benevoleIdList: Array<number>;
   competencesIdList: Array<number>;
   description: string;
+  isMeal: boolean;
 }
 export type CreneauJSON = Omit<ICreneau, "event"> & {
   event: EventJSON;
@@ -45,14 +46,15 @@ class Creneau implements ICreneau {
   }
   competencesIdList: number[];
   description: string;
+  isMeal: boolean;
 
   constructor(obj: ICreneau) {
-    if (!isString(obj.event.id) || obj.event.id == "") {
+    if (!(typeof obj.event.id == "string") || obj.event.id == "") {
       throw new TypeError(
         "missing argument 0 (event.id:String) when calling function Creneau.constructor"
       );
     }
-    if (!isString(obj.event.ressourceId) || obj.event.ressourceId == "") {
+    if (!(typeof obj.event.ressourceId == "string") || obj.event.ressourceId == "") {
       throw new TypeError(
         "missing argument 0 (event.ressourceId:String) when calling function Creneau.constructor"
       );
@@ -64,6 +66,7 @@ class Creneau implements ICreneau {
     this.maxAttendee = "maxAttendee" in obj ? obj.maxAttendee : 0;
     this._benevoleIdList = "benevoleIdList" in obj ? obj.benevoleIdList : [];
     this.competencesIdList = "competencesIdList" in obj ? obj.competencesIdList : [];
+    this.isMeal = "isMeal" in obj ? obj.isMeal : false;
     this.updateEventContent();
   }
   get id(): string {
@@ -104,7 +107,7 @@ class Creneau implements ICreneau {
       dayjs(this.event.end).format("HH:mm")
     );
   }
-  updateEventContent() {
+  updateEventContent(): void {
     const missingbenevole = Math.max(this.minAttendee - this.benevoleIdList.length, 0);
     const spanClass = this.benevoleIdList.length == 0 ? "red" : missingbenevole > 0 ? "orange" : "";
     this.event.content = `<div class="bubble ${spanClass}">${missingbenevole}</div>`;
@@ -120,6 +123,25 @@ class Creneau implements ICreneau {
       benevoleIdList: this.benevoleIdList,
       competencesIdList: this.competencesIdList,
       description: this.description,
+      isMeal: this.isMeal,
+    };
+  }
+  toMealSlot(): MealSlot {
+    return {
+      id: this.event.id,
+      startTime: this.start.toISOString(),
+      endTime: this.end.toISOString(),
+      maxAttendee: this.maxAttendee,
+    };
+  }
+  toTimeslotRaw(): TimeslotRaw {
+    return {
+      id: this.event.id,
+      startTime: this.start.toISOString(),
+      endTime: this.end.toISOString(),
+      skillsId: this.competencesIdList,
+      minAttendee: this.minAttendee,
+      maxAttendee: this.maxAttendee,
     };
   }
   static fromJSON(input: string | CreneauJSON): Creneau {

+ 4 - 4
src/models/Evenement.ts

@@ -21,18 +21,18 @@ export default class Evenement {
   set start(date: Date) {
     this.startingDate = dayjs(date);
   }
-  get start() {
+  get start(): Date {
     return this.startingDate.toDate();
   }
 
   set end(date: Date) {
     this.endingDate = dayjs(date);
   }
-  get end() {
+  get end(): Date {
     return this.endingDate.toDate();
   }
 
-  toJSON() {
+  toJSON(): IEvenement {
     return {
       name: this.name,
       uuid: this.uuid,
@@ -40,7 +40,7 @@ export default class Evenement {
       end: this.endingDate.toISOString(),
     };
   }
-  static fromJSON(input: string | IEvenement) {
+  static fromJSON(input: string | IEvenement): Evenement {
     let obj: IEvenement;
     if (typeof input == "string") {
       obj = JSON.parse(input);

+ 43 - 0
src/models/SolverInput.ts

@@ -0,0 +1,43 @@
+// Type used to communicate the problem to be optimized to the serveur
+export type Skill = {
+  id: number;
+  name: string;
+  isTeachable: boolean;
+  isPreference: boolean;
+};
+export type AssignementPair = {
+  slotId: string;
+  volonteerId: number;
+};
+export type TimeslotRaw = {
+  id: string;
+  startTime: string;
+  endTime: string;
+  skillsId: Array<number>;
+  minAttendee: number;
+  maxAttendee: number;
+};
+export type VolonteerRaw = {
+  id: number;
+  skillsId: Array<number>;
+};
+export type MealSlot = {
+  id: string;
+  startTime: string;
+  endTime: string;
+  maxAttendee: number;
+};
+export type SolverInput = {
+  constraints: Array<Skill>;
+  mealAssignements: Array<AssignementPair>;
+  volonteerList: Array<VolonteerRaw>;
+  mealSlots: Array<MealSlot>;
+  timeslots: Array<TimeslotRaw>;
+  assignements: Array<AssignementPair>;
+};
+
+export type SolverOutput = {
+  assignements: Array<AssignementPair>;
+  message: string;
+  score: string;
+};

+ 7 - 1
src/store/Mutations.ts

@@ -4,7 +4,7 @@ import Creneau from "@/models/Creneau";
 import Evenement from "@/models/Evenement";
 import { Ressource } from "jc-timeline";
 import { MutationTree } from "vuex";
-import { State, StateJSON } from "./State";
+import { State } from "./State";
 
 export enum MutationTypes {
   resetState = "resetState",
@@ -15,6 +15,7 @@ export enum MutationTypes {
   editCreneau = "editCreneau",
   addBenevole2Creneau = "addBenevole2Creneau",
   removeBenevole2Creneau = "removeBenevole2Creneau",
+  clearBenevole2Creneau = "clearBenevole2Creneau",
 
   addCreneauGroup = "addCreneauGroup",
   removeCreneauGroup = "removeCreneauGroup",
@@ -58,6 +59,7 @@ export type Mutations<S = State> = {
 
   [MutationTypes.addBenevole2Creneau](state: S, payload: CreneauPairing): void;
   [MutationTypes.removeBenevole2Creneau](state: S, payload: CreneauPairing): void;
+  [MutationTypes.clearBenevole2Creneau](state: S): void;
 
   [MutationTypes.addBenevole](state: S, payload: Benevole): void;
   [MutationTypes.removeBenevole](state: S, payload: Benevole): void;
@@ -117,6 +119,10 @@ export const mutations: MutationTree<State> & Mutations = {
     if (creneau)
       creneau.benevoleIdList = creneau.benevoleIdList.filter((id) => id !== pair.benevoleId);
   },
+  [MutationTypes.clearBenevole2Creneau](state) {
+    state.benevoleList.forEach((b) => (b.creneauIdList = []));
+    state.creneauList.forEach((b) => (b.benevoleIdList = []));
+  },
   // Creneau Group Management
   [MutationTypes.reorderCreneauGroup](state, payload) {
     state.creneauGroupList = payload;

+ 1 - 1
src/utils/Toast.ts

@@ -103,7 +103,7 @@ class Toast {
         "--completion",
         Math.round((ellapsed / this.options.displayLength) * 100) + "%"
       );
-      setTimeout(() => this.update(), 200);
+      setTimeout(() => this.update(), 20);
     } else {
       this.dismiss();
     }

+ 8 - 0
src/utils/toast.css

@@ -60,4 +60,12 @@
     width: var(--completion);
     height:3px;
     background-color: rgb(160, 140, 179);
+}
+
+.toast.error{
+  color: #282e3a;
+  background-color: #f9ccd4;
+}
+.toast.error::after{
+  background-color: #b60022;
 }

+ 1 - 1
src/views/Home.vue

@@ -18,7 +18,7 @@
       <styled-input label="Fin" :modelValue="evenement.end" @input="dateListener($event, 'end')" />
 
       <div class="actions" style="text-align: center">
-        <button class="btn success" @click="exportStateToJson">
+        <button class="btn success" @click="$emit('solve')">
           <i class="material-icons">play_arrow</i> Résoudre la plannification
         </button>
       </div>

+ 2 - 1
src/views/Planning.vue

@@ -62,7 +62,6 @@ import { v4 as uuidv4 } from "uuid";
 import { defineComponent } from "vue";
 import Timeline from "jc-timeline/lib/Timeline";
 import { Event as jcEvent } from "jc-timeline/lib/Event";
-import "jc-timeline";
 import EditeurCreneau from "@/components/EditeurCreneau.vue";
 import EditeurLigne from "@/components/EditeurCreneauGroup.vue";
 import { Ressource } from "jc-timeline";
@@ -75,6 +74,7 @@ import toast from "@/utils/Toast";
 import * as dayjs from "dayjs";
 import "dayjs/locale/fr";
 dayjs.locale("fr");
+
 type changePayload = {
   items: Array<jcEvent>;
 };
@@ -126,6 +126,7 @@ export default defineComponent({
             benevoleIdList: [],
             competencesIdList: [],
             description: "",
+            isMeal: false,
           });
           this.$store.commit(MutationTypes.addCreneau, creneau);
 

+ 1 - 1
src/views/PlanningPersonnel.vue

@@ -36,7 +36,7 @@ import { defineComponent } from "vue";
 import Creneau from "@/models/Creneau";
 import Benevole from "@/models/Benevole";
 import AutocompleteValues from "@/models/AutocompleteOptions";
-import { Dictionary } from "node_modules/@types/lodash";
+import { Dictionary } from "lodash";
 import dayjs from "dayjs";
 
 export default defineComponent({

+ 8 - 0
vue.config.js

@@ -0,0 +1,8 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+/*const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
+
+module.exports = {
+  configureWebpack: {
+    plugins: [new BundleAnalyzerPlugin()],
+  },
+};*/