| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- <template>
- <div class="header">
- <div class="logo"></div>
- <h1 class="appname">BDLG planner</h1>
- <nav class="tabs floating">
- <router-link class="tab" active-class="selected" to="/"> Accueil </router-link>
- <router-link class="tab" active-class="selected" to="/evenement">Planning</router-link>
- <router-link class="tab" active-class="selected" to="/competences">
- Gestion des compétences
- </router-link>
- <router-link class="tab" active-class="selected" to="/benevoles">
- Gestion des bénévoles
- </router-link>
- <router-link class="tab" active-class="selected" to="/planningIndividuel">
- Planning Individuel
- </router-link>
- </nav>
- </div>
- <div class="container">
- <router-view
- @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 v-if="showExplanation" class="modal" @click="showExplanation = false">
- <div class="modal-content" v-html="explanation" @click="(e) => e.stopPropagation()"></div>
- </div>
- </div>
- </template>
- <script lang="ts">
- import { defineComponent } from "vue";
- import toast, { Toast } from "./utils/Toast";
- import "@/assets/css/tabs.css";
- import ConstraintTranslation from "@/assets/ConstraintTranslation";
- import Evenement from "./models/Evenement";
- import Benevole from "./models/Benevole";
- import Creneau from "./models/Creneau";
- import Competence from "./models/Competence";
- import {
- AssignementPair,
- SolverInput,
- SolverOutput,
- JustificationObject,
- ScoreExplanation,
- HardConstraint,
- } 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,
- explanation: "",
- showExplanation: false,
- lastToast: null as null | Toast,
- };
- },
- mounted() {
- const previousState = window.localStorage.getItem("activeState");
- toast({ html: "coucou" });
- if (previousState) {
- this.importJsonState(JSON.parse(previousState), false);
- }
- window.onbeforeunload = () => {
- this.localSave();
- };
- },
- methods: {
- localSave() {
- window.localStorage.setItem("activeState", JSON.stringify(this.$store.getters.getJSONState));
- },
- clearToast() {
- if (this.lastToast) {
- this.lastToast.dismiss();
- this.lastToast = null;
- }
- },
- exportStateToJson(): void {
- const obj: StateJSON = this.$store.getters.getJSONState;
- const mimeType = "data:text/json;charset=utf-8";
- const dummy = document.createElement("a");
- dummy.href = mimeType + ", " + encodeURI(JSON.stringify(obj));
- dummy.download =
- "Planning-" + this.$store.state.evenement.name + dayjs().format("-YYYY-MM-DD[.json]");
- document.body.appendChild(dummy);
- 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) => {
- const obj: AssignementPair = { volonteerId: id, slotId: c.id };
- if (c.fixedAttendee) obj.isFixed = true;
- return obj;
- });
- 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<br>" + error.toString(), classes: "error" });
- this.optimisationInProgress = false;
- });
- },
- updatePlanningWithNewPairing(data: SolverOutput): void {
- this.optimisationInProgress = false;
- // Display message from the solver
- toast({
- html: "Le planning a été mis à jour <br>Score : " + data.score,
- classes: "success",
- displayLength: 10000,
- });
- if (data.message) toast({ html: data.message, classes: "warning", displayLength: 10000 });
- this.displayExplanation(data.explanation);
- // 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,
- });
- }
- },
- displayExplanation(explanation: string) {
- // Display score explanation if any
- const scoreExplanation: ScoreExplanation = JSON.parse(explanation);
- let scoreExplanationHTML = "";
- let k: HardConstraint;
- for (k in scoreExplanation) {
- // Title of the section
- scoreExplanationHTML += `<h4>${ConstraintTranslation[k]}</h4>`;
- // Content of the section splitted by group
- scoreExplanationHTML += scoreExplanation[k].map((list) => {
- const pairs = list
- .map((o) => {
- if (typeof o === "string" || o instanceof String) {
- return o;
- } else {
- const parseObj = o as JustificationObject;
- const slot = this.$store.getters.getCreneauById(parseObj.slotId);
- const benevole = this.$store.getters.getBenevoleById(parseObj.volonteerId);
- return `Bénevole :\t${benevole?.shortame}<br>Créneau:\t${slot?.title} ${slot?.horaire}`;
- }
- })
- .join("<br>Et<br>");
- if (pairs != "") {
- return `<p>${pairs}</p>`;
- }
- return "";
- });
- // End of the section
- scoreExplanationHTML += "<hr>";
- }
- this.explanation = scoreExplanationHTML;
- this.clearToast();
- this.lastToast = toast({
- html: `<span>Ouvrir l'explication du score</span><i class="material-icons">close<i>`,
- displayLength: Infinity,
- classes: "action",
- });
- this.lastToast.element.firstElementChild?.addEventListener(
- "click",
- () => (this.showExplanation = true)
- );
- this.lastToast.element.lastElementChild?.addEventListener("click", this.clearToast);
- },
- importJsonState(obj: StateJSON, preserve = false) {
- // Remove previous content and load the main event title
- if (preserve == false) {
- 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],
- });
- }
- }
- // Import constraint
- obj.competences.forEach((c) => {
- this.$store.commit(MutationTypes.addConstraint, Competence.fromJSON(c));
- });
- // Import Benevoles
- obj.benevoles.forEach((b) => {
- this.$store.commit(MutationTypes.addBenevole, Benevole.fromJSON(b));
- });
- // Import creneau group
- const dict: { [k: string]: RessourceJSON } = {};
- obj.creneauGroups.forEach((element) => {
- dict[element.id] = element;
- });
- // map parent to children
- const creneauGroups = obj.creneauGroups.map((ressource) => {
- const iRessource: IRessource = { ...ressource };
- if (iRessource.parentId) {
- if (iRessource.parentId in dict) {
- iRessource.parent = dict[iRessource.parentId];
- } else {
- throw new Error("Missing parent of creneau group : " + ressource.id);
- }
- }
- return iRessource;
- });
- // Push the items to this application
- creneauGroups.forEach((r) => {
- this.$store.commit(MutationTypes.addCreneauGroup, new Ressource(r));
- });
- // Import Creneau
- for (let c of obj.creneaux) {
- const creneau = Creneau.fromJSON(c);
- this.$store.commit(MutationTypes.addCreneau, creneau);
- // add the creneau to the corresponding benevole
- for (const id of creneau.benevoleIdList) {
- const b = this.$store.getters.getBenevoleById(id);
- if (b) {
- this.$store.commit(MutationTypes.editBenevole, {
- id: id,
- field: "creneauIdList",
- value: Array.from(new Set([...b.creneauIdList, creneau.id])),
- });
- } else {
- throw new Error(`The benevole ${id} was not find `);
- }
- }
- }
- },
- },
- });
- </script>
- <style>
- .toast.action > span {
- cursor: pointer;
- color: var(--color-primary-600);
- }
- .toast.action > i.material-icons {
- cursor: pointer;
- margin-left: 16px;
- margin-right: -8px;
- font-size: 18px;
- }
- </style>
|