浏览代码

implement planning page

tripeur 4 年之前
父节点
当前提交
8a2de9e40d

+ 1 - 1
.env.staging

@@ -1,3 +1,3 @@
 NODE_ENV=production
 VUE_APP_TITLE=[Staging] BDLG Planner  
-VUE_APP_API_URL=localhost:8080/
+VUE_APP_API_URL=http://localhost:8080/

+ 1 - 0
package.json

@@ -4,6 +4,7 @@
   "private": true,
   "scripts": {
     "serve": "vue-cli-service serve",
+    "stage": "vue-cli-service build --mode staging",
     "build": "vue-cli-service build",
     "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"

+ 22 - 81
src/PlannerApp.vue

@@ -42,13 +42,11 @@
 </template>
 <script lang="ts">
 import { defineComponent } from "vue";
-import toast, { Toast } from "./utils/Toast";
+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 Evenement from "@/models/Evenement";
+import Creneau from "@/models/Creneau";
 import {
   AssignementPair,
   SolverInput,
@@ -56,18 +54,17 @@ import {
   JustificationObject,
   ScoreExplanation,
   HardConstraint,
-} from "./models/SolverInput";
-import Ressource, { IRessource, RessourceJSON } from "jc-timeline/lib/Ressource";
+} from "@/models/SolverInput";
 import updatePlanningVersions from "@/mixins/updatePlanningVersions";
-import { MutationTypes } from "./store/Mutations";
+import importJsonState from "@/mixins/ImportJsonState";
+import { MutationTypes } from "@/store/Mutations";
 import dayjs from "dayjs";
-import { StateJSON } from "./store/State";
+import { StateJSON } from "@/store/State";
 
 const API_URL = process.env.VUE_APP_API_URL;
 
-const keyofEvent: Array<keyof Evenement> = ["name", "uuid", "start", "end"];
 export default defineComponent({
-  mixins: [updatePlanningVersions],
+  mixins: [updatePlanningVersions, importJsonState],
   data() {
     return {
       optimisationInProgress: false,
@@ -77,7 +74,9 @@ export default defineComponent({
     };
   },
   mounted() {
-    fetch(`${API_URL}api/evenements`)
+    const url = `${API_URL}api/evenements`;
+    console.log(url);
+    fetch(url)
       .then((response) => {
         if (response.status == 200) {
           return response.json();
@@ -90,8 +89,8 @@ export default defineComponent({
     const previousState = window.localStorage.getItem("activeState");
     toast({ html: "Bienvenue" });
     if (previousState) {
-      this.importJsonState(JSON.parse(previousState), false);
-      if (this.$route.path != "/planner") this.$router.push({ name: "Evenement" });
+      this.importState(JSON.parse(previousState));
+      if (this.$route.path == "/planner") this.$router.push({ name: "Evenement" });
     }
     window.onbeforeunload = () => {
       this.localSave();
@@ -164,7 +163,16 @@ export default defineComponent({
       };
       newState.evenement.name = "Nouvel événement";
       this.save();
+      this.importState(newState);
+    },
+    importState(newState: StateJSON) {
+      const prevUuid = this.$store.state.evenement.uuid;
       this.importJsonState(newState);
+      if (this.$store.state.evenement.uuid != prevUuid) {
+        this.updatePlanningVersions();
+      }
+      // Remove past score explanation.
+      this.clearToast();
     },
     exportStateToJson(): void {
       const obj: StateJSON = this.$store.getters.getJSONState;
@@ -292,73 +300,6 @@ export default defineComponent({
         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) {
-        const prevUuid = this.$store.state.evenement.uuid;
-        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],
-          });
-        }
-        if (e.uuid != prevUuid) {
-          this.updatePlanningVersions();
-        }
-      }
-      // 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 `);
-          }
-        }
-      }
-      // Remove past score explanation.
-      this.clearToast();
-      this.$router.push({ name: "Evenement" });
-    },
   },
 });
 </script>

+ 111 - 0
src/PlanningPage.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="header">
+    <div class="logo" />
+    <div class="appname">BDLG planner</div>
+  </div>
+  <div class="container">
+    <div v-if="isLoading" class="loading">Chargement en cours <br /><dots /></div>
+    <template v-else-if="hasData">
+      <h1 style="text-align: center">{{ title }}</h1>
+      <planning />
+    </template>
+    <div class="no-info" v-else>
+      <i class="material-icons">travel_explore</i>
+      <div>Planning introuvable<br />Veuillez vérifier votre lien</div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from "vue";
+import Planning from "@/views/PlanningPersonnel.vue";
+import Dots from "@/components/Dots.vue";
+import importJsonState from "@/mixins/ImportJsonState";
+import Toast from "./utils/Toast";
+
+const API_URL = process.env.VUE_APP_API_URL;
+const uuidv4check = /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
+
+export default defineComponent({
+  mixins: [importJsonState],
+  components: { Planning, Dots },
+  data() {
+    return { isLoading: true, hasData: false };
+  },
+  computed: {
+    title(): string {
+      return this.$store.state.evenement.name;
+    },
+  },
+  mounted() {
+    Toast({ html: "Bienvenue" });
+    const uuid = document.location.pathname.split("/").reverse()[0];
+    if (uuidv4check.test(uuid)) {
+      const url = `${API_URL}api/evenements/${uuid}`;
+      fetch(url)
+        .then((res) => {
+          if (res.status == 200) {
+            return res.json();
+          } else {
+            throw Error(res.statusText);
+          }
+        })
+
+        .then((data) => {
+          this.hasData = true;
+          this.isLoading = false;
+          this.importJsonState(data);
+        })
+        .catch((reason) => {
+          this.isLoading = false;
+          Toast({ html: reason });
+        });
+    } else {
+      this.isLoading = false;
+    }
+  },
+});
+</script>
+
+<style scoped>
+.container {
+  display: flex;
+  justify-content: center;
+  flex-direction: column;
+  margin: 8px;
+}
+
+.no-info {
+  display: flex;
+  flex-direction: column;
+  font-size: 1.5em;
+  margin: 16px;
+}
+.no-info > .material-icons {
+  font-size: 9em;
+  line-height: 1.1em;
+  text-align: center;
+  color: var(--color-neutral-800);
+}
+.no-info > div {
+  text-align: center;
+  line-height: 1.5em;
+
+  color: var(--color-neutral-200);
+}
+.loading {
+  text-align: center;
+  font-size: 30px;
+  margin: 40px 8px;
+}
+
+@media (min-width: 600px) {
+  .no-info {
+    font-size: 2em;
+  }
+  .loading {
+    margin-top: 60px;
+    font-size: 40px;
+  }
+}
+</style>

+ 4 - 1
src/components/CreneauViewer.vue

@@ -166,7 +166,7 @@ export default defineComponent({
   border-bottom: 1px solid var(--color-accent-600);
 }
 .agenda-creneau-time {
-  padding-right: 2rem;
+  padding-right: 8px;
 }
 
 .agenda-creneau-title {
@@ -209,6 +209,9 @@ export default defineComponent({
   margin: 0px 0px 8px;
 }
 @media (min-width: 600px) {
+  .agenda-creneau-time {
+    padding-right: 2rem;
+  }
   .agenda-creneau-details--title {
     width: 16.6%;
     margin-bottom: 12px;

+ 58 - 0
src/components/Dots.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="dots"><i class="dot"></i><i class="dot"></i><i class="dot"></i></div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from "vue";
+
+export default defineComponent({
+  setup() {
+    return {};
+  },
+});
+</script>
+
+<style scoped>
+@keyframes dots-jump {
+  0%,
+  60%,
+  100% {
+    top: 50%;
+  }
+  30% {
+    top: 0px;
+  }
+}
+.dot {
+  all: initial;
+  position: absolute;
+  display: inline-block;
+  font-size: inherit;
+  width: 0.6em;
+  height: 0.6em;
+  left: 0.2em;
+  background-color: var(--color-primary-400);
+  border-radius: 50%;
+  animation-name: dots-jump;
+  animation-duration: 1.4s;
+  animation-iteration-count: infinite;
+  animation-fill-mode: both;
+}
+.dot:nth-child(2) {
+  animation-delay: 0.2s;
+  left: 1.2em;
+}
+.dot:nth-child(3) {
+  animation-delay: 0.4s;
+  left: 2.2em;
+}
+.dots {
+  all: initial;
+  display: inline-block;
+  position: relative;
+  font-size: inherit;
+  width: 3em;
+  margin: 1em;
+  height: 1em;
+}
+</style>

+ 75 - 0
src/mixins/ImportJsonState.ts

@@ -0,0 +1,75 @@
+import Benevole from "@/models/Benevole";
+import Competence from "@/models/Competence";
+import Creneau from "@/models/Creneau";
+import Evenement from "@/models/Evenement";
+import { MutationTypes } from "@/store/Mutations";
+import { StateJSON } from "@/store/State";
+import { IRessource, Ressource, RessourceJSON } from "jc-timeline";
+import { defineComponent } from "vue";
+
+const keyofEvent: Array<keyof Evenement> = ["name", "uuid", "start", "end"];
+
+export default defineComponent({
+  methods: {
+    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 (const 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 `);
+          }
+        }
+      }
+    },
+  },
+});

+ 0 - 0
src/main.ts → src/planner.ts


+ 9 - 0
src/planningPage.ts

@@ -0,0 +1,9 @@
+import { createApp } from "vue";
+import App from "@/PlanningPage.vue";
+import router from "./router";
+import { store } from "./store/Store";
+import "@/assets/css/main.css";
+import "@/assets/css/button.css";
+
+const app = createApp(App).use(router).use(store);
+app.mount("#app");

+ 3 - 0
src/utils/toast.css

@@ -85,3 +85,6 @@
 .toast.success::after {
   background-color: #097350;
 }
+.toast > a {
+  color: var(--color-accent-400);
+}

+ 19 - 0
src/views/Evenement.vue

@@ -37,6 +37,11 @@
           @change="importJsonState"
         />
       </div>
+      <div class="actions">
+        <button class="btn primary small" @click="copyLink">
+          <i class="material-icons">content_copy</i>Lien pour les bénévoles
+        </button>
+      </div>
     </div>
     <div v-if="showModal" class="modal" @click="toggleModal">
       <div class="modal-content" @click="(e) => e.stopPropagation()">
@@ -82,6 +87,7 @@ import datepicker from "@/components/date-picker.vue";
 import EvenementDataTable from "@/components/EvenementDataTable.vue";
 import updatePlanningVersions from "@/mixins/updatePlanningVersions";
 import fetchPlanningVersion from "@/mixins/fetchPlanningVersion";
+import Toast from "@/utils/Toast";
 
 const API_URL = process.env.VUE_APP_API_URL;
 
@@ -128,6 +134,14 @@ export default defineComponent({
     },
   },
   methods: {
+    copyLink() {
+      const link = `${API_URL}planning/display/${this.evenement.uuid}`;
+      navigator.clipboard.writeText(link);
+      Toast({
+        html: `Lien copié <a href="http://${link}" target="_blank"><i class="material-icons" style="vertical-align: middle;margin-left: 8px;">launch</i></a>`,
+        displayLength: 5000,
+      });
+    },
     loadVersion(version: EvenementVersion) {
       this.fetchPlanningVersions(
         `${API_URL}api/evenements/history/${version.uuid}/content/${version.id}`
@@ -199,6 +213,11 @@ export default defineComponent({
   padding: 16px;
   margin-bottom: 32px;
 }
+.actions {
+  margin-top: 8px;
+  text-align: center;
+}
+
 h2 {
   margin-top: 0px;
   margin-left: 8px;

+ 1 - 0
src/views/Home.vue

@@ -46,6 +46,7 @@ export default defineComponent({
       });
       this.fetchPlanningVersions(`${API_URL}api/evenements/${version.uuid}`).then(() => {
         toast.dismiss();
+        this.$router.push({ name: "Evenement" });
         Toast({
           html: `Planning "${version.name}" chargé`,
           classes: "success",

+ 74 - 35
src/views/PlanningPersonnel.vue

@@ -1,28 +1,41 @@
 <template>
-  <div class="container">
-    <div class="planning-container">
-      <auto-complete-input
-        class="search-benevole"
-        id="benevole-selection-input"
-        label="Choisir un bénévole"
-        :autocompleteList="autocompleteData"
-        :strictAutocomplete="true"
-        v-model="benevoleId"
-      />
-      <div
-        class="daily-agenda"
-        v-for="(thisDayCreneauList, day) in personalCreneauListPerDay"
-        :key="day"
-      >
-        <div class="day-header">{{ day }}</div>
-        <div>
-          <creneau-viewer
-            v-for="creneau in thisDayCreneauList"
-            :key="creneau.id"
-            :creneau="creneau"
-            :current-benevole="currentBenevole"
-            @contact="onContact"
-          ></creneau-viewer>
+  <div class="planning-container">
+    <auto-complete-input
+      class="search-benevole"
+      id="benevole-selection-input"
+      label="Choisir un bénévole"
+      :autocompleteList="autocompleteData"
+      :strictAutocomplete="true"
+      v-model="benevoleId"
+    />
+    <div
+      class="daily-agenda"
+      v-for="(thisDayCreneauList, day) in personalCreneauListPerDay"
+      :key="day"
+    >
+      <div class="day-header">{{ day }}</div>
+      <div>
+        <creneau-viewer
+          v-for="creneau in thisDayCreneauList"
+          :key="creneau.id"
+          :creneau="creneau"
+          :current-benevole="currentBenevole"
+          @contact="onContact"
+        ></creneau-viewer>
+      </div>
+    </div>
+    <div class="modal" v-if="showContact" @click="closeModal">
+      <div class="modal-content" ref="modal">
+        <div class="contact-header">
+          <i class="material-icons">person</i>
+          <div>{{ showContact.fullname }}</div>
+        </div>
+        <div class="contact-detail">
+          <i class="material-icons">phone</i>{{ showContact.formatPhone }}
+        </div>
+        <div class="contact-detail"><i class="material-icons">email</i>{{ showContact.email }}</div>
+        <div class="contact-detail">
+          <i class="material-icons">music_note</i>{{ getFanfare(showContact) }}
         </div>
       </div>
     </div>
@@ -45,6 +58,7 @@ export default defineComponent({
     return {
       currentBenevole: undefined as Benevole | undefined,
       benevoleId: "",
+      showContact: undefined as Benevole | undefined,
     };
   },
   watch: {
@@ -89,6 +103,11 @@ export default defineComponent({
     },
   },
   methods: {
+    closeModal(event: MouseEvent) {
+      if (!(this.$refs.modal as HTMLElement).contains(event.target as HTMLElement)) {
+        this.showContact = undefined;
+      }
+    },
     updateCurrentBenevole: function () {
       const id = parseInt(this.benevoleId);
       const bList = this.benevoleList.filter((b) => b.id == id);
@@ -97,7 +116,14 @@ export default defineComponent({
       }
     },
     onContact: function (benevole: Benevole) {
-      this.$emit("contact", benevole);
+      this.showContact = benevole;
+    },
+    getFanfare(benevole: Benevole): string {
+      return benevole.competenceIdList
+        .map((id) => this.$store.getters.getCompetenceById(id))
+        .filter((o) => o && o.name.startsWith("Fanfare"))
+        .map((o) => o?.name.slice(8))
+        .join(", ");
     },
   },
 });
@@ -134,8 +160,9 @@ export default defineComponent({
   padding: 12px;
 }
 .contact-header {
-  background: MidnightBlue;
+  background: var(--color-accent-400);
   color: white;
+  margin-bottom: 8px;
 }
 
 .contact-header > i {
@@ -145,20 +172,32 @@ export default defineComponent({
 }
 
 .contact-header > div {
-  text-align: center;
   font-size: 3rem;
+  text-align: center;
   margin-top: -2.5rem;
+  padding: 8px;
 }
 
-.contact-detail > li {
-  display: flex;
+@media (max-width: 600px) {
+  .contact-header > div {
+    font-size: 1.2rem;
+    margin-top: -1.2rem;
+  }
+  .contact-header > i {
+    font-size: 8rem;
+  }
+  .modal-content {
+    width: 90%;
+  }
 }
 
-.contact-detail > li > i {
-  width: 2rem;
-  font-size: 1.6rem;
-  display: inline-block;
-  text-align: center;
-  margin-right: 1rem;
+.contact-detail {
+  border: solid 1px var(--color-neutral-800);
+  padding: 8px;
+  display: flex;
+  align-items: center;
+}
+.contact-detail > i {
+  margin-right: 8px;
 }
 </style>

+ 7 - 2
vue.config.js

@@ -6,12 +6,17 @@
  */
 module.exports = {
   pages: {
-    index: { entry: "./src/main.ts", title: "BDLG planner", filename: "planner/index.html" },
+    index: { entry: "./src/planner.ts", title: "BDLG planner", filename: "planner/index.html" },
     login: {
       entry: "./src/login.ts",
-      title: "BDLG planner Connexion",
+      title: "BDLG planner - Connexion",
       filename: "login.html",
     },
+    display: {
+      entry: "./src/planningPage.ts",
+      title: "BDLG planner - Visualisation",
+      filename: "planning/display/index.html",
+    },
   },
   outputDir:
     process.env.NODE_ENV === "production"