App.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <template>
  2. <div class="header">
  3. <div class="logo"></div>
  4. <h1 class="appname">BDLG planner</h1>
  5. <nav class="tabs floating">
  6. <router-link class="tab" active-class="selected" to="/"> Accueil </router-link>
  7. <router-link class="tab" active-class="selected" to="/evenement">Planning</router-link>
  8. <router-link class="tab" active-class="selected" to="/competences">
  9. Gestion des compétences
  10. </router-link>
  11. <router-link class="tab" active-class="selected" to="/benevoles">
  12. Gestion des bénévoles
  13. </router-link>
  14. <router-link class="tab" active-class="selected" to="/planningIndividuel">
  15. Planning Individuel
  16. </router-link>
  17. </nav>
  18. </div>
  19. <div class="container">
  20. <router-view
  21. @export="exportStateToJson"
  22. @import="(e) => importJsonState(e, false)"
  23. @localSave="localSave"
  24. @solve="solve"
  25. />
  26. <div v-if="optimisationInProgress" class="modal">
  27. <div class="modal-content">
  28. <h3>Optimisation en cours</h3>
  29. <p>L'ordinateur calcul un meilleur planning</p>
  30. <div class="spinner"><span class="material-icons"> sync </span></div>
  31. </div>
  32. </div>
  33. </div>
  34. </template>
  35. <script lang="ts">
  36. import { defineComponent } from "vue";
  37. import toast from "./utils/Toast";
  38. import "@/assets/css/tabs.css";
  39. import Evenement from "./models/Evenement";
  40. import Benevole from "./models/Benevole";
  41. import Creneau from "./models/Creneau";
  42. import Competence from "./models/Competence";
  43. import { SolverInput, SolverOutput } from "./models/SolverInput";
  44. import Ressource, { IRessource, RessourceJSON } from "jc-timeline/lib/Ressource";
  45. import { MutationTypes } from "./store/Mutations";
  46. import dayjs from "dayjs";
  47. import { StateJSON } from "./store/State";
  48. const keyofEvent: Array<keyof Evenement> = ["name", "uuid", "start", "end"];
  49. export default defineComponent({
  50. data() {
  51. return { optimisationInProgress: false };
  52. },
  53. mounted() {
  54. const previousState = window.localStorage.getItem("activeState");
  55. if (previousState) {
  56. this.importJsonState(JSON.parse(previousState), false);
  57. }
  58. window.onbeforeunload = () => {
  59. this.localSave();
  60. };
  61. },
  62. methods: {
  63. localSave() {
  64. window.localStorage.setItem("activeState", JSON.stringify(this.$store.getters.getJSONState));
  65. },
  66. exportStateToJson(): void {
  67. const obj: StateJSON = this.$store.getters.getJSONState;
  68. const mimeType = "data:text/json;charset=utf-8";
  69. const dummy = document.createElement("a");
  70. dummy.href = mimeType + ", " + encodeURI(JSON.stringify(obj));
  71. dummy.download =
  72. "Planning-" + this.$store.state.evenement.name + dayjs().format("-YYYY-MM-DD[.json]");
  73. document.body.appendChild(dummy);
  74. dummy.click();
  75. setTimeout(() => document.body.removeChild(dummy), 10000);
  76. },
  77. solve() {
  78. const meals = this.$store.state.creneauList.filter((o) => o.isMeal);
  79. const slots = this.$store.state.creneauList.filter((o) => !o.isMeal);
  80. const timeslots = slots.map((o) => o.toTimeslotRaw());
  81. const mealSlots = meals.map((o) => o.toMealSlot());
  82. const volonteerList = this.$store.state.benevoleList.map((b) => b.toVolonteeRaw());
  83. const constraints = this.$store.state.competenceList.map((c) => c.toSkill());
  84. const getAssignementPair = (c: Creneau) =>
  85. c.benevoleIdList.map((id) => {
  86. return { volonteerId: id, slotId: c.id };
  87. });
  88. const assignements = slots.flatMap(getAssignementPair);
  89. const mealAssignements = meals.flatMap(getAssignementPair);
  90. const body: SolverInput = {
  91. constraints,
  92. timeslots,
  93. mealSlots,
  94. volonteerList,
  95. assignements,
  96. mealAssignements,
  97. };
  98. const options = {
  99. method: "POST",
  100. body: JSON.stringify(body),
  101. headers: { "Content-type": "application/json; charset=UTF-8" },
  102. };
  103. this.optimisationInProgress = true;
  104. fetch(`http://localhost:8080/planning/solve`, options)
  105. .then((response) => response.json())
  106. .then(this.updatePlanningWithNewPairing)
  107. .catch((error) => {
  108. toast({ html: "Pas de réponse du serveur<br>" + error.toString(), classes: "error" });
  109. this.optimisationInProgress = false;
  110. });
  111. },
  112. updatePlanningWithNewPairing(data: SolverOutput) {
  113. this.optimisationInProgress = false;
  114. // Display message from the solver
  115. toast({
  116. html: "Le planning a été mis à jour <br>Score : " + data.score,
  117. classes: "success",
  118. displayLength: 10000,
  119. });
  120. data.message
  121. .split("\n")
  122. .filter((s) => s != "")
  123. .forEach((s) => toast({ html: s, classes: "warning", displayLength: 10000 }));
  124. // Remove previous timeslot assignement
  125. this.$store.commit(MutationTypes.clearBenevole2Creneau, undefined);
  126. // Add new timeslot assignement
  127. for (const pair of data.assignements) {
  128. this.$store.commit(MutationTypes.addBenevole2Creneau, {
  129. creneauId: pair.slotId,
  130. benevoleId: pair.volonteerId,
  131. });
  132. }
  133. },
  134. importJsonState(obj: StateJSON, preserve = false) {
  135. console.log(obj);
  136. // Remove previous content and load the main event title
  137. if (preserve == false) {
  138. this.$store.commit(MutationTypes.resetState, undefined);
  139. const e = Evenement.fromJSON(obj.evenement);
  140. for (const k of keyofEvent) {
  141. this.$store.commit(MutationTypes.editEvenement, {
  142. field: k,
  143. value: e[k],
  144. });
  145. }
  146. }
  147. // Import constraint
  148. obj.competences.forEach((c) => {
  149. this.$store.commit(MutationTypes.addConstraint, Competence.fromJSON(c));
  150. });
  151. // Import Benevoles
  152. obj.benevoles.forEach((b) => {
  153. this.$store.commit(MutationTypes.addBenevole, Benevole.fromJSON(b));
  154. });
  155. // Import creneau group
  156. const dict: { [k: string]: RessourceJSON } = {};
  157. obj.creneauGroups.forEach((element) => {
  158. dict[element.id] = element;
  159. });
  160. // map parent to children
  161. const creneauGroups = obj.creneauGroups.map((ressource) => {
  162. const iRessource: IRessource = { ...ressource };
  163. if (iRessource.parentId) {
  164. if (iRessource.parentId in dict) {
  165. iRessource.parent = dict[iRessource.parentId];
  166. } else {
  167. throw new Error("Missing parent of creneau group : " + ressource.id);
  168. }
  169. }
  170. return iRessource;
  171. });
  172. // Push the items to this application
  173. creneauGroups.forEach((r) => {
  174. this.$store.commit(MutationTypes.addCreneauGroup, new Ressource(r));
  175. });
  176. // Import Creneau
  177. for (let c of obj.creneaux) {
  178. const creneau = Creneau.fromJSON(c);
  179. this.$store.commit(MutationTypes.addCreneau, creneau);
  180. // add the creneau to the corresponding benevole
  181. for (const id of creneau.benevoleIdList) {
  182. const b = this.$store.getters.getBenevoleById(id);
  183. if (b) {
  184. this.$store.commit(MutationTypes.editBenevole, {
  185. id: id,
  186. field: "creneauIdList",
  187. value: Array.from(new Set([...b.creneauIdList, creneau.id])),
  188. });
  189. } else {
  190. throw new Error(`The benevole ${id} was not find `);
  191. }
  192. }
  193. }
  194. },
  195. },
  196. });
  197. </script>
  198. <style>
  199. .modal {
  200. display: flex;
  201. justify-content: center;
  202. position: fixed;
  203. top: 0;
  204. left: 0;
  205. right: 0;
  206. bottom: 0;
  207. background: rgba(0, 0, 0, 0.5);
  208. z-index: 100;
  209. }
  210. .modal-content {
  211. background: white;
  212. padding: 16px 32px;
  213. margin-top: 100px;
  214. height: min-content;
  215. border-radius: 2px;
  216. }
  217. .spinner {
  218. color: var(--color-accent-400);
  219. text-align: center;
  220. }
  221. .spinner > .material-icons {
  222. font-size: 3rem;
  223. animation: spin 2s linear infinite;
  224. }
  225. @keyframes spin {
  226. 0% {
  227. transform: rotateZ(360deg);
  228. }
  229. 100% {
  230. transform: rotateZ(0deg);
  231. }
  232. }
  233. </style>