App.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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 v-if="showExplanation" class="modal" @click="showExplanation = false">
  34. <div class="modal-content" v-html="explanation" @click="(e) => e.stopPropagation()"></div>
  35. </div>
  36. </div>
  37. </template>
  38. <script lang="ts">
  39. import { defineComponent } from "vue";
  40. import toast, { Toast } from "./utils/Toast";
  41. import "@/assets/css/tabs.css";
  42. import ConstraintTranslation from "@/assets/ConstraintTranslation";
  43. import Evenement from "./models/Evenement";
  44. import Benevole from "./models/Benevole";
  45. import Creneau from "./models/Creneau";
  46. import Competence from "./models/Competence";
  47. import {
  48. AssignementPair,
  49. SolverInput,
  50. SolverOutput,
  51. JustificationObject,
  52. ScoreExplanation,
  53. HardConstraint,
  54. } from "./models/SolverInput";
  55. import Ressource, { IRessource, RessourceJSON } from "jc-timeline/lib/Ressource";
  56. import { MutationTypes } from "./store/Mutations";
  57. import dayjs from "dayjs";
  58. import { StateJSON } from "./store/State";
  59. const keyofEvent: Array<keyof Evenement> = ["name", "uuid", "start", "end"];
  60. export default defineComponent({
  61. data() {
  62. return {
  63. optimisationInProgress: false,
  64. explanation: "",
  65. showExplanation: false,
  66. lastToast: null as null | Toast,
  67. };
  68. },
  69. mounted() {
  70. const previousState = window.localStorage.getItem("activeState");
  71. toast({ html: "coucou" });
  72. if (previousState) {
  73. this.importJsonState(JSON.parse(previousState), false);
  74. }
  75. window.onbeforeunload = () => {
  76. this.localSave();
  77. };
  78. },
  79. methods: {
  80. localSave() {
  81. window.localStorage.setItem("activeState", JSON.stringify(this.$store.getters.getJSONState));
  82. },
  83. clearToast() {
  84. if (this.lastToast) {
  85. this.lastToast.dismiss();
  86. this.lastToast = null;
  87. }
  88. },
  89. exportStateToJson(): void {
  90. const obj: StateJSON = this.$store.getters.getJSONState;
  91. const mimeType = "data:text/json;charset=utf-8";
  92. const dummy = document.createElement("a");
  93. dummy.href = mimeType + ", " + encodeURI(JSON.stringify(obj));
  94. dummy.download =
  95. "Planning-" + this.$store.state.evenement.name + dayjs().format("-YYYY-MM-DD[.json]");
  96. document.body.appendChild(dummy);
  97. dummy.click();
  98. setTimeout(() => document.body.removeChild(dummy), 10000);
  99. },
  100. solve() {
  101. const meals = this.$store.state.creneauList.filter((o) => o.isMeal);
  102. const slots = this.$store.state.creneauList.filter((o) => !o.isMeal);
  103. const timeslots = slots.map((o) => o.toTimeslotRaw());
  104. const mealSlots = meals.map((o) => o.toMealSlot());
  105. const volonteerList = this.$store.state.benevoleList.map((b) => b.toVolonteeRaw());
  106. const constraints = this.$store.state.competenceList.map((c) => c.toSkill());
  107. const getAssignementPair = (c: Creneau) =>
  108. c.benevoleIdList.map((id) => {
  109. const obj: AssignementPair = { volonteerId: id, slotId: c.id };
  110. if (c.fixedAttendee) obj.isFixed = true;
  111. return obj;
  112. });
  113. const assignements = slots.flatMap(getAssignementPair);
  114. const mealAssignements = meals.flatMap(getAssignementPair);
  115. const body: SolverInput = {
  116. constraints,
  117. timeslots,
  118. mealSlots,
  119. volonteerList,
  120. assignements,
  121. mealAssignements,
  122. };
  123. const options = {
  124. method: "POST",
  125. body: JSON.stringify(body),
  126. headers: { "Content-type": "application/json; charset=UTF-8" },
  127. };
  128. this.optimisationInProgress = true;
  129. fetch(`http://localhost:8080/planning/solve`, options)
  130. .then((response) => response.json())
  131. .then(this.updatePlanningWithNewPairing)
  132. .catch((error) => {
  133. toast({ html: "Pas de réponse du serveur<br>" + error.toString(), classes: "error" });
  134. this.optimisationInProgress = false;
  135. });
  136. },
  137. updatePlanningWithNewPairing(data: SolverOutput): void {
  138. this.optimisationInProgress = false;
  139. // Display message from the solver
  140. toast({
  141. html: "Le planning a été mis à jour <br>Score : " + data.score,
  142. classes: "success",
  143. displayLength: 10000,
  144. });
  145. if (data.message) toast({ html: data.message, classes: "warning", displayLength: 10000 });
  146. this.displayExplanation(data.explanation);
  147. // Remove previous timeslot assignement
  148. this.$store.commit(MutationTypes.clearBenevole2Creneau, undefined);
  149. // Add new timeslot assignement
  150. for (const pair of data.assignements) {
  151. this.$store.commit(MutationTypes.addBenevole2Creneau, {
  152. creneauId: pair.slotId,
  153. benevoleId: pair.volonteerId,
  154. });
  155. }
  156. },
  157. displayExplanation(explanation: string) {
  158. // Display score explanation if any
  159. const scoreExplanation: ScoreExplanation = JSON.parse(explanation);
  160. let scoreExplanationHTML = "";
  161. let k: HardConstraint;
  162. for (k in scoreExplanation) {
  163. // Title of the section
  164. scoreExplanationHTML += `<h4>${ConstraintTranslation[k]}</h4>`;
  165. // Content of the section splitted by group
  166. scoreExplanationHTML += scoreExplanation[k].map((list) => {
  167. const pairs = list
  168. .map((o) => {
  169. if (typeof o === "string" || o instanceof String) {
  170. return o;
  171. } else {
  172. const parseObj = o as JustificationObject;
  173. const slot = this.$store.getters.getCreneauById(parseObj.slotId);
  174. const benevole = this.$store.getters.getBenevoleById(parseObj.volonteerId);
  175. return `Bénevole :\t${benevole?.shortame}<br>Créneau:\t${slot?.title} ${slot?.horaire}`;
  176. }
  177. })
  178. .join("<br>Et<br>");
  179. if (pairs != "") {
  180. return `<p>${pairs}</p>`;
  181. }
  182. return "";
  183. });
  184. // End of the section
  185. scoreExplanationHTML += "<hr>";
  186. }
  187. this.explanation = scoreExplanationHTML;
  188. this.clearToast();
  189. this.lastToast = toast({
  190. html: `<span>Ouvrir l'explication du score</span><i class="material-icons">close<i>`,
  191. displayLength: Infinity,
  192. classes: "action",
  193. });
  194. this.lastToast.element.firstElementChild?.addEventListener(
  195. "click",
  196. () => (this.showExplanation = true)
  197. );
  198. this.lastToast.element.lastElementChild?.addEventListener("click", this.clearToast);
  199. },
  200. importJsonState(obj: StateJSON, preserve = false) {
  201. // Remove previous content and load the main event title
  202. if (preserve == false) {
  203. this.$store.commit(MutationTypes.resetState, undefined);
  204. const e = Evenement.fromJSON(obj.evenement);
  205. for (const k of keyofEvent) {
  206. this.$store.commit(MutationTypes.editEvenement, {
  207. field: k,
  208. value: e[k],
  209. });
  210. }
  211. }
  212. // Import constraint
  213. obj.competences.forEach((c) => {
  214. this.$store.commit(MutationTypes.addConstraint, Competence.fromJSON(c));
  215. });
  216. // Import Benevoles
  217. obj.benevoles.forEach((b) => {
  218. this.$store.commit(MutationTypes.addBenevole, Benevole.fromJSON(b));
  219. });
  220. // Import creneau group
  221. const dict: { [k: string]: RessourceJSON } = {};
  222. obj.creneauGroups.forEach((element) => {
  223. dict[element.id] = element;
  224. });
  225. // map parent to children
  226. const creneauGroups = obj.creneauGroups.map((ressource) => {
  227. const iRessource: IRessource = { ...ressource };
  228. if (iRessource.parentId) {
  229. if (iRessource.parentId in dict) {
  230. iRessource.parent = dict[iRessource.parentId];
  231. } else {
  232. throw new Error("Missing parent of creneau group : " + ressource.id);
  233. }
  234. }
  235. return iRessource;
  236. });
  237. // Push the items to this application
  238. creneauGroups.forEach((r) => {
  239. this.$store.commit(MutationTypes.addCreneauGroup, new Ressource(r));
  240. });
  241. // Import Creneau
  242. for (let c of obj.creneaux) {
  243. const creneau = Creneau.fromJSON(c);
  244. this.$store.commit(MutationTypes.addCreneau, creneau);
  245. // add the creneau to the corresponding benevole
  246. for (const id of creneau.benevoleIdList) {
  247. const b = this.$store.getters.getBenevoleById(id);
  248. if (b) {
  249. this.$store.commit(MutationTypes.editBenevole, {
  250. id: id,
  251. field: "creneauIdList",
  252. value: Array.from(new Set([...b.creneauIdList, creneau.id])),
  253. });
  254. } else {
  255. throw new Error(`The benevole ${id} was not find `);
  256. }
  257. }
  258. }
  259. },
  260. },
  261. });
  262. </script>
  263. <style>
  264. .toast.action > span {
  265. cursor: pointer;
  266. color: var(--color-primary-600);
  267. }
  268. .toast.action > i.material-icons {
  269. cursor: pointer;
  270. margin-left: 16px;
  271. margin-right: -8px;
  272. font-size: 18px;
  273. }
  274. </style>