Browse Source

complete working planning

tripeur 4 năm trước cách đây
mục cha
commit
cd89d84c15

+ 1 - 1
package-lock.json

@@ -6904,7 +6904,7 @@
       "dev": true
     },
     "jc-timeline": {
-      "version": "git+http://gitlab.jaquin.fr/clovis/jc-timeline.git#64792b425ad2697566f39e8b3b12b3f258a6a54d",
+      "version": "git+http://gitlab.jaquin.fr/clovis/jc-timeline.git#888f6c4bd4c9d96c893e1d244514f1a63ff6f3c6",
       "from": "git+http://gitlab.jaquin.fr/clovis/jc-timeline.git",
       "requires": {
         "dayjs": "^1.10.4",

+ 11 - 1
src/assets/css/button.css

@@ -37,7 +37,7 @@
   opacity: 0.5;
   pointer-events: none;
 }
-.btn svg, .btn .material-icons {
+.btn svg, .btn:not(.icon) .material-icons {
   fill: currentColor;
   width: 1rem;
   height: 1rem;
@@ -45,18 +45,28 @@
   margin-left: calc(-0.125rem);
   margin-right: 0.5rem;
 }
+
 .btn.xsmall {
   padding: 2px 0.5rem;
   font-size: 0.875rem;
   min-width: 1rem;
   line-height: 1rem;
 }
+
 .btn.small {
   padding: calc(0.125rem + 1px) .7rem;
   font-size: 0.875rem;
   min-width: 2rem;
   line-height: 1.5rem;
 }
+.btn.icon{
+}
+.btn.icon.small{
+  padding: calc(0.25rem + 1px) ;
+}
+.btn.icon.small .material-icons{
+  font-size: 1.25rem;
+}
 .btn.large {
   padding: calc(0.75rem - 1px) 1.5rem;
   font-size: 1rem;

+ 14 - 1
src/assets/css/main.css

@@ -109,7 +109,20 @@ body{
     -moz-osx-font-smoothing: grayscale;
   }  
 
-  
+.bubble{
+  height: 20px;
+  width: 20px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: absolute;
+  right: -4px;
+  bottom: -4px;
+  border-radius: 50%;
+  font-size: 12px;
+  background: red;
+  font-weight: bold;
+}
 .formcontrol {
   all: initial;
   display: block;

+ 1 - 1
src/assets/css/multiple-select.css

@@ -6,7 +6,7 @@ position: relative;
 display: block;
 width: 100%;
 position: relative;
-height: 2.5rem;
+height: 2rem;
 }
 .select-multiple > .dropdown-options {
   display: none;

+ 12 - 12
src/components/AutoCompleteInput.vue

@@ -6,6 +6,7 @@
     >
     <div
       class="select-multiple"
+      ref="container"
       :class="{ 'select-multiple--expanded': showAutocomplete }"
       @click="openDropDown"
     >
@@ -98,10 +99,11 @@ export default defineComponent({
       e.stopPropagation();
     },
     closeDropDown: function (e: MouseEvent) {
-      if (!this.$el.contains(e.target)) {
+      if (!(this.$refs["container"] as HTMLElement).contains(e.target as Node)) {
         this.expanded = false;
         window.removeEventListener("click", this.closeDropDown);
         e.stopPropagation();
+        if (this.value == "") this.$emit("update:modelValue", this.value);
       }
     },
     toggle(v: AutocompleteValues, e: MouseEvent) {
@@ -112,21 +114,19 @@ export default defineComponent({
     },
     onKeyup: function (e: KeyboardEvent): void {
       if (e.key === "Enter") {
-        if (this.activeIndex > -1 && this.activeIndex < this.displayedAutocomplete.length) {
+        if (this.value === "") {
+          this.$emit("update:modelValue", "");
+          this.expanded = false;
+        } else if (this.activeIndex > -1 && this.activeIndex < this.displayedAutocomplete.length) {
           const v = this.displayedAutocomplete[this.activeIndex];
           this.value = v.name;
           this.$emit("update:modelValue", v.id);
           this.expanded = false;
-        } else {
-          if (this.value === "") {
-            this.$emit("update:modelValue", "");
-            this.expanded = false;
-          } else if (this.strictAutocomplete) {
-            toast({
-              html: "Vous ne pouvez ajouter que des valeurs prédéfinies",
-              classes: "red",
-            });
-          }
+        } else if (this.strictAutocomplete) {
+          toast({
+            html: "Vous ne pouvez ajouter que des valeurs prédéfinies",
+            classes: "red",
+          });
         }
       }
       if (e.key === "ArrowUp") {

+ 182 - 131
src/components/EditeurCreneau.vue

@@ -1,131 +1,138 @@
 <template>
   <div>
     <h3 class="center-align">Modifier un créneau</h3>
-    <div class="row center-align">
-      <a class="btn-small" v-on:click="emitCreationOrder"
-        ><i class="material-icons right">create</i>Nouveau</a
-      >
-      <a
-        class="btn-small"
+    <div class="actions">
+      <button class="btn small primary" v-on:click="emitCreationOrder">
+        <i class="material-icons right">create</i>Nouveau
+      </button>
+      <button
+        class="btn small primary"
         :class="{ disabled: creneau === undefined }"
         v-on:click="emitDuplicateOrder"
-        ><i class="material-icons right">content_copy</i>Dupliquer</a
       >
-      <a
-        class="btn-small red"
+        <i class="material-icons right">content_copy</i>Dupliquer
+      </button>
+      <button
+        class="btn small error"
         :class="{ disabled: creneau === null }"
         v-on:click="emitDeleteOrder"
-        style="margin-top: 4px"
-        ><i class="material-icons right">delete_forever</i>Supprimer</a
       >
+        <i class="material-icons right">delete_forever</i>Supprimer
+      </button>
     </div>
     <div class="center-align" v-if="creneau === undefined">Veuillez selectioner un creneau.</div>
     <form v-else>
-      <div class="input-field col s12">
-        <input
-          id="last_name"
-          type="text"
-          class="validate"
-          value="creneau.title"
-          @input="inputListener('title')"
-        />
-        <label for="last_name">Titre</label>
-      </div>
+      <styled-input
+        label="Titre"
+        id="last_name"
+        type="text"
+        :modelValue="creneau.title"
+        @input="inputListener($event, 'title')"
+      />
+      <styled-input
+        class="small"
+        label="Date"
+        id="creneauDate"
+        type="text"
+        placeholder="yyyy-mm-dd"
+        v-model.lazy="jour"
+      />
+      <styled-input
+        class="small"
+        label="Heure"
+        id="creneauHeure"
+        type="text"
+        placeholder="hh:mm"
+        v-model.lazy="heure"
+      />
+      <styled-input
+        class="small"
+        label="Durée (min)"
+        id="creneauDuree"
+        type="number"
+        v-model.lazy="duree"
+      />
+      <styled-input
+        class="small"
+        label="Heure fin"
+        id="disabled"
+        type="text"
+        :modelValue="endHour"
+        disabled
+      />
+      <styled-input
+        class="small"
+        label="Bénévole minimum"
+        id="minAttendee"
+        type="number"
+        :modelValue="creneau.minAttendee"
+        @input="inputListener($event, 'minAttendee')"
+      />
+      <styled-input
+        class="small"
+        label="Bénévole minimum"
+        id="minAttendee"
+        type="number"
+        :optional="true"
+        :modelValue="creneau.maxAttendee"
+        @input="inputListener($event, 'maxAttendee')"
+      />
+      <styled-input
+        label="Pénibilité"
+        id="penibility"
+        type="number"
+        class="small"
+        :modelValue="creneau.penibility"
+        @input="inputListener($event, 'penibility')"
+      />
 
-      <div class="input-field col s6">
-        <input id="creneauDate" type="text" class="datepicker" v-model.lazy="jour" />
-        <label for="creneauDate">Date</label>
-      </div>
-      <div class="input-field col s3">
-        <input id="creneauHeure" type="text" class="timepicker" v-model.lazy="heure" />
-        <label for="creneauHeure">Heure</label>
-      </div>
-      <div class="input-field col s3">
-        <input id="creneauDuree" type="number" class="validate" v-model="duree" />
-        <label for="creneauDuree">Duree (min)</label>
-      </div>
-      <div class="input-field col s6">
-        <input
-          id="penibility"
-          type="number"
-          class="validate"
-          :value="creneau.penibility"
-          @input="inputListener('penibility')"
-        />
-        <label for="penibility">Pénébilité</label>
-      </div>
-      <div class="input-field col s3">
-        <input disabled id="disabled" type="text" class="validate" v-model="endHour" />
-        <label for="disabled">Heure fin</label>
-      </div>
-      <div class="input-field col s6">
-        <input
-          id="minAttendee"
-          type="number"
-          class="validate"
-          :value="creneau.minAttendee"
-          @input="inputListener('minAttendee')"
-        />
-        <label for="minAttendee">Bénévole minimum</label>
-      </div>
-      <div class="input-field col s6">
-        <input
-          id="maxAttendee"
-          type="number"
-          class="validate"
-          :value="creneau.maxAttendee"
-          @input="inputListener('maxAttendee')"
-        />
-        <label for="maxAttendee">Bénévole maximum (opt)</label>
-      </div>
-      <div class="input-field col s12">
-        <textarea
-          id="description"
-          type="text"
-          class="materialize-textarea"
-          v-model="creneau.description"
-        ></textarea>
-        <label for="description">Description</label>
-      </div>
-      <div class="col s12">
-        <chips-input
-          title="Compétences & préférences associées"
-          id="compétence_selection"
-          place-holder="Choisir une condition"
-          secondary-placeholder="+ compétence"
-          :autocomplete-list="autocompleteCompetencesList"
-          :strict-autocomplete="true"
-          :value="creneau.competencesIdList"
-          @input="inputListener('competencesIdList')"
-        ></chips-input>
-      </div>
-      <div class="col s12">
-        <chips-input
-          title="Bénévoles"
-          id="bénevole_selection"
-          place-holder="Choisir un bénévole"
-          secondary-placeholder="+ bénévole"
-          :autocomplete-list="autocompleteBenevolesList"
-          :strict-autocomplete="true"
-          :value="creneau.benevoleIdList"
-          @input="inputListener('benevoleIdList')"
-        ></chips-input>
-      </div>
+      <styled-input
+        label="Description"
+        id="description"
+        type="textarea"
+        class="materialize-textarea"
+        :modelValue="creneau.description"
+        @input="inputListener($event, 'description')"
+      >
+      </styled-input>
+      <chips-input
+        label="Compétences & préférences associées"
+        id="compétence_selection"
+        placeholder="Choisir une condition"
+        secondary-placeholder="+ compétence"
+        :autocomplete-list="autocompleteCompetencesList"
+        :strict-autocomplete="true"
+        :value="creneau.competencesIdList"
+        @input="inputListener($event, 'competencesIdList')"
+      ></chips-input>
+      <chips-input
+        label="Bénévoles"
+        id="bénevole_selection"
+        placeholder="Choisir un bénévole"
+        secondary-placeholder="+ bénévole"
+        :autocomplete-list="autocompleteBenevolesList"
+        :strict-autocomplete="true"
+        :value="creneau.benevoleIdList"
+        @input="inputListener($event, 'benevoleIdList')"
+      ></chips-input>
     </form>
   </div>
 </template>
 
 <script lang="ts">
-import { defineComponent } from "vue";
+import { defineComponent, PropType } from "vue";
 import Creneau from "@/models/Creneau";
-import { MutationTypes } from "@/store/Mutations";
 import AutocompleteOptions from "@/models/AutocompleteOptions";
+import styledInput from "./input.vue";
+import chipsInput from "../components/SelectChipInput.vue";
+import dayjs from "dayjs";
 
 export default defineComponent({
   name: "EditeurCreneau",
+  components: { "chips-input": chipsInput, styledInput },
   props: {
-    creneauId: {
-      type: String,
+    creneau: {
+      type: Object as PropType<Creneau>,
     },
   },
   data: function () {
@@ -136,7 +143,10 @@ export default defineComponent({
     };
   },
   watch: {
-    creneauId() {
+    "creneau.start": function () {
+      this.updateForm();
+    },
+    "creneau.end": function () {
       this.updateForm();
     },
     jour: function () {
@@ -153,21 +163,30 @@ export default defineComponent({
     duration(): number {
       return parseFloat(this.duree) ?? 0;
     },
-    formStartTime(): number {
-      const input = this.jour + "T" + this.heure;
-      return new Date(input).getTime();
+    concatStart(): string {
+      return (
+        this.jour +
+        "T" +
+        this.heure
+          .split(":")
+          .map((c) => ("0" + c).slice(-2))
+          .join(":")
+      );
+    },
+    validStart(): boolean {
+      return /\d{4}-\d{1,2}-\d{1,2}/.test(this.jour) && dayjs(this.concatStart).isValid();
     },
-    endStartTime(): number {
-      return this.formStartTime + this.duration * 60 * 1000;
+    startDate(): Date {
+      return new Date(this.concatStart);
+    },
+    endDate(): Date {
+      return dayjs(this.startDate).add(this.duration, "m").toDate();
     },
     endHour(): string {
-      return new Date(this.endStartTime).toLocaleTimeString().substring(0, 5);
+      return this.endDate.toTimeString().substring(0, 5);
     },
     validDuree(): boolean {
-      return isNaN(parseInt(this.duree));
-    },
-    creneau(): Creneau | undefined {
-      return this.$store.getters.getCreneauById(this.creneauId || "");
+      return !isNaN(parseFloat(this.duree));
     },
     autocompleteBenevolesList(): Array<AutocompleteOptions> {
       return this.$store.state.benevoleList.map((benevole) => {
@@ -182,31 +201,34 @@ export default defineComponent({
   },
   methods: {
     updateDates: function (): void {
-      if (this.creneauId) {
-        this.updateCreneau("start", new Date(this.formStartTime));
-        this.updateCreneau("end", new Date(this.endStartTime));
+      if (this.creneau && this.validDuree && this.validStart) {
+        if (Math.abs(this.startDate.getTime() - this.creneau.start.getTime()) > 1000)
+          this.updateCreneau("start", this.startDate);
+        if (Math.abs(this.endDate.getTime() - this.creneau.end.getTime()) > 1000)
+          this.updateCreneau("end", this.endDate);
       }
     },
     updateCreneau<K extends keyof Creneau>(field: K, value: Creneau[K]) {
-      if (this.creneauId)
-        this.$store.commit(MutationTypes.editCreneau, {
-          id: this.creneauId,
+      if (this.creneau) {
+        const payload = {
+          id: this.creneau.id,
           field: field,
           value: value,
-        });
+        };
+        this.$emit("edit", payload);
+      }
     },
-    inputListener(field: keyof Creneau): (e: InputEvent) => void {
-      return (e: InputEvent) => {
-        this.updateCreneau(field, (e.target as HTMLInputElement).value);
-      };
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    inputListener(event: any, field: keyof Creneau) {
+      this.updateCreneau(field, event.target.value);
     },
     updateForm: function () {
       if (this.creneau) {
-        const startDate = this.creneau.event.start;
-        this.jour = startDate.toISOString().substr(0, 10);
-        this.heure = startDate.toTimeString().substr(0, 5);
+        const startDate = dayjs(this.creneau.start);
+        this.jour = startDate.format("YYYY-MM-DD");
+        this.heure = startDate.format("HH:mm");
         this.duree = Math.round(
-          (this.creneau.event.end.getTime() - this.creneau.event.start.getTime()) / 1000 / 60
+          (this.creneau.end.getTime() - this.creneau.start.getTime()) / 1000 / 60
         ).toString();
       }
     },
@@ -220,7 +242,36 @@ export default defineComponent({
       this.$emit("delete", this.creneau);
     },
   },
+  mounted() {
+    this.updateForm();
+  },
 });
 </script>
 
-<style scoped></style>
+<style scoped>
+.actions {
+  display: flex;
+  justify-content: center;
+}
+.actions > * {
+  margin: 8px;
+}
+.small {
+  max-width: calc(50% - 8px);
+}
+form > * {
+  width: 100%;
+}
+form {
+  display: flex;
+  justify-content: space-between;
+  flex-wrap: wrap;
+}
+h3 {
+  margin-top: 0px;
+  margin-bottom: 8px;
+}
+.formcontrol:not(:last-child) {
+  margin-bottom: 4px;
+}
+</style>

+ 0 - 1
src/components/EditeurCreneauGroup.vue

@@ -54,7 +54,6 @@
 <script lang="ts">
 import { defineComponent } from "vue";
 import { Ressource } from "jc-timeline/lib/Ressource";
-import { MutationTypes } from "@/store/Mutations";
 import AutoCompleteInput from "./AutoCompleteInput.vue";
 import styledInput from "./input.vue";
 import AutocompleteOptions from "@/models/AutocompleteOptions";

+ 67 - 57
src/components/SelectChipInput.vue

@@ -1,53 +1,58 @@
 <template>
-  <div class="select-multiple" :class="{ 'select-multiple--expanded': expanded }">
-    <div class="select-multiple-value" @click="openDropDown">
-      <span v-if="checkedItems.length === 0"></span>
-      <div v-for="item of displayedItems" class="ds-chip" :key="item.id">
-        <span class="ds-chip-label">{{ item.text }}</span>
-        <button aria-label="Clear" class="ds-chip-remove-btn" @click="toggle(item.id, $event)" />
+  <div class="formcontrol">
+    <label class="formcontrol-label" for="" v-if="optional || label"
+      >{{ label }}
+      <div class="formcontrol-optional" v-if="optional">Optional</div></label
+    >
+    <div ref="container" class="select-multiple" :class="{ 'select-multiple--expanded': expanded }">
+      <div class="select-multiple-value" @click="openDropDown">
+        <span v-if="checkedItems.length === 0"></span>
+        <div v-for="item of displayedItems" class="ds-chip" :key="item.id">
+          <span class="ds-chip-label">{{ item.text }}</span>
+          <button aria-label="Clear" class="ds-chip-remove-btn" @click="toggle(item.id, $event)" />
+        </div>
+        <div v-if="checkedItems.length > maxItemDisplayed" class="ds-chip">
+          <div class="ds-chip-label">+{{ checkedItems.length - maxItemDisplayed }}</div>
+        </div>
+        <input
+          class="select-multiple-input"
+          ref="input"
+          :placeholder="currentPlaceholder"
+          v-model="inputValue"
+          :size="Math.max(inputValue.length, currentPlaceholder.length)"
+          @keyup="keyUp"
+        />
       </div>
-      <div v-if="checkedItems.length > maxItemDisplayed" class="ds-chip">
-        <div class="ds-chip-label">+{{ checkedItems.length - maxItemDisplayed }}</div>
+      <div class="dropdown-options">
+        <div
+          v-for="(item, index) in filteredItems"
+          :class="{
+            'select--checked': item.isChecked,
+            'select--active': index == activeIndex,
+          }"
+          :key="item.value"
+          @click="toggle(item.index, $event)"
+          v-html="highlight(item.text)"
+        ></div>
       </div>
-      <input
-        class="select-multiple-input"
-        ref="input"
-        :placeholder="placeholder"
-        v-model="inputValue"
-        :size="Math.max(inputValue.length, placeholder.length)"
-        @keyup="keyUp"
-      />
-    </div>
-    <div class="dropdown-options">
-      <div
-        v-for="(item, index) in filteredItems"
-        :class="{
-          'select--checked': item.isChecked,
-          'select--active': index == activeIndex,
-        }"
-        :key="item.id"
-        @click="toggle(item.id, $event)"
-        v-html="highlight(item.text)"
-      ></div>
     </div>
   </div>
 </template>
 
 <script lang="ts">
+import AutocompleteValues from "@/models/AutocompleteOptions";
 import { defineComponent, PropType } from "vue";
 interface selectItem {
-  id: number;
+  index: number;
   isChecked: boolean;
-  value: any;
-  text: string;
-}
-interface selectOption {
-  value: any;
+  value: string;
   text: string;
 }
 export default defineComponent({
   name: "selectChip",
   props: {
+    label: String,
+    optional: Boolean,
     // Values selected by the component
     valueModel: {
       type: Array as PropType<Array<string>>,
@@ -55,20 +60,20 @@ export default defineComponent({
     },
     /*
      * Potential values that could be selected by the user
-     * type Array<String> | Array<Option>
-     * Option {
-     *   value:Any,
-     *   text:String ,
-     * }
+     * type Array<String> | Array<AutocompleteValues>
      */
-    options: {
-      type: Array as PropType<Array<string | selectOption>>,
+    autocompleteList: {
+      type: Array as PropType<Array<string | AutocompleteValues>>,
       default: () => [],
     },
     placeholder: {
       type: String,
       default: "Choose among the list",
     },
+    secondaryPlaceholder: {
+      type: String,
+      default: "+ ",
+    },
     // Number of selected item displayed before displaying an ellipsis appear
     maxItemDisplayed: {
       type: Number,
@@ -100,22 +105,24 @@ export default defineComponent({
       e.stopPropagation();
     },
     updateItems(): void {
-      this.items = this.options.map((o: selectOption | string, idx: number) => {
-        let itemValue: any, text: string;
-        if (typeof o == "object") {
-          itemValue = "value" in o ? o.value : o;
-          text = "text" in o ? o.text : itemValue;
-        } else {
-          itemValue = o;
-          text = o;
+      this.items = this.autocompleteList.map(
+        (o: AutocompleteValues | string, idx: number): selectItem => {
+          let id: string, txt: string;
+          if (typeof o == "object") {
+            id = "id" in o ? o.id : o;
+            txt = "name" in o ? o.name : id;
+          } else {
+            id = o;
+            txt = o;
+          }
+          return {
+            index: idx,
+            isChecked: this.valueModel.includes(id),
+            value: id,
+            text: txt,
+          };
         }
-        return {
-          id: idx,
-          isChecked: this.valueModel.includes(itemValue),
-          value: itemValue,
-          text: text,
-        };
-      });
+      );
     },
     openDropDown: function (e: MouseEvent) {
       this.expanded = true;
@@ -124,7 +131,7 @@ export default defineComponent({
       e.stopPropagation();
     },
     closeDropDown: function (e: MouseEvent) {
-      if (!this.$el.contains(e.target)) {
+      if (!(this.$refs["container"] as HTMLElement).contains(e.target as Node)) {
         this.expanded = false;
         window.removeEventListener("click", this.closeDropDown);
         e.stopPropagation();
@@ -196,6 +203,9 @@ export default defineComponent({
     selectedItem(): selectItem | undefined {
       return this.filteredItems[this.activeIndex];
     },
+    currentPlaceholder(): string {
+      return this.checkedItems.length > 0 ? this.secondaryPlaceholder : this.placeholder;
+    },
   },
   beforeMount: function () {
     this.updateItems();

+ 2 - 6
src/components/date-picker.vue

@@ -9,10 +9,7 @@
       @click="openPicker"
       @focus="openPicker"
     />
-    <div
-      class="ds-datepicker-container"
-      :class="[{ show: isPickerOpen }, position]"
-    >
+    <div class="ds-datepicker-container" :class="[{ show: isPickerOpen }, position]">
       <div class="ds-datepicker">
         <div class="ds-datepicker-prev" @click="prevMonth" tabindex="-1"></div>
         <div class="ds-datepicker-current">{{ pickerTitle }}</div>
@@ -122,8 +119,7 @@ export default defineComponent({
         this.textValue = val === "Invalid Date" ? "" : val.slice(0, 10);
       }
       if (l < oldVal.length) {
-        this.textValue =
-          oldVal.charAt(oldVal.length - 1) === "/" ? val.slice(0, -1) : val;
+        this.textValue = oldVal.charAt(oldVal.length - 1) === "/" ? val.slice(0, -1) : val;
       } else if ([4, 7].includes(l)) {
         this.textValue = val + "/";
       }

+ 17 - 4
src/components/input.vue

@@ -5,8 +5,21 @@
       <div class="formcontrol-optional" v-if="optional">Optional</div>
     </label>
     <div class="formcontrol-control">
+      <textarea
+        class="input"
+        :id="id"
+        cols="30"
+        rows="10"
+        v-if="type == 'textarea'"
+        :placeholder="placeholder"
+        v-model="value"
+        :required="required"
+        :disabled="disabled"
+      ></textarea>
       <input
+        v-else
         class="input"
+        id="id"
         :type="type"
         :placeholder="placeholder"
         v-model="value"
@@ -40,7 +53,7 @@ export default defineComponent({
   props: {
     id: String,
     label: String,
-    modelValue: String,
+    modelValue: { type: [String, Number] },
     placeholder: { type: String, default: "" },
     helpText: String,
     helpLinkHref: String,
@@ -89,13 +102,13 @@ export default defineComponent({
     modelValue: function (val: string): void {
       this.value = val;
     },
-    value(new_val, old_val) {
-      if (new_val != old_val) this.$emit("update:modelValue", new_val);
+    value(new_val: string): void {
+      if (new_val !== this.modelValue) this.$emit("update:modelValue", new_val);
     },
   },
   methods: {},
   mounted(): void {
-    this.value = this.modelValue || "";
+    this.value = this.modelValue + "";
   },
 });
 </script>

+ 3 - 1
src/main.ts

@@ -5,4 +5,6 @@ import { store } from "./store/Store";
 import "@/assets/css/main.css";
 import "@/assets/css/button.css";
 
-createApp(App).use(router).use(store).mount("#app");
+const app = createApp(App).use(router).use(store);
+app.config.isCustomElement = (tag) => tag.startsWith("jc-");
+app.mount("#app");

+ 13 - 9
src/models/Benevole.ts

@@ -51,22 +51,26 @@ export default class Benevole {
   get shortame(): string {
     return this.name + " (" + this.fanfare + ")";
   }
+  get imgPlaceholder(): string {
+    return (
+      "holder.js/32x32?theme=social&text=" +
+      this.name.charAt(0).toUpperCase() +
+      this.surname.charAt(0).toUpperCase()
+    );
+  }
   get fanfare(): string {
     const defaultValue = "Exte";
+    return defaultValue;
+    /*
     if (this.competenceIdList.length == 0) {
       return defaultValue;
     } else {
       const output = this.fanfareList.join(",");
       return output == "" ? defaultValue : output;
-    }
-  }
-  get imgPlaceholder(): string {
-    return (
-      "holder.js/32x32?theme=social&text=" +
-      this.name.charAt(0).toUpperCase() +
-      this.surname.charAt(0).toUpperCase()
-    );
+        }
+      */
   }
+  /*
   get fanfareList() {
     return competenceList
       .filter((o) => o.name.startsWith("Fanfare"))
@@ -101,7 +105,7 @@ export default class Benevole {
       content.join(", ") +
       "</span>";
     return span;
-  }
+  }*/
   static fromObject(obj: IBenevole) {
     let id: number;
     if (isNaN(obj.id)) {

+ 39 - 10
src/models/Creneau.ts

@@ -13,10 +13,33 @@ export interface ICreneau {
 }
 class Creneau implements ICreneau {
   event: Event;
-  penibility: number;
-  minAttendee: number;
+
+  public get title(): string {
+    return this.event.title;
+  }
+  public set title(value: string) {
+    this.event.title = value;
+  }
+
+  _minAttendee: number;
+  public get minAttendee(): number {
+    return this._minAttendee;
+  }
+  public set minAttendee(value: number) {
+    this._minAttendee = value;
+    this.updateEventContent();
+  }
   maxAttendee: number;
-  benevoleIdList: number[];
+
+  penibility: number;
+  _benevoleIdList: number[];
+  public get benevoleIdList(): number[] {
+    return this._benevoleIdList;
+  }
+  public set benevoleIdList(value: number[]) {
+    this._benevoleIdList = value;
+    this.updateEventContent();
+  }
   competencesIdList: number[];
   description: string;
 
@@ -34,10 +57,11 @@ class Creneau implements ICreneau {
     this.event = obj.event;
     this.description = "description" in obj ? obj.description : "";
     this.penibility = "penibility" in obj ? obj.penibility : 12;
-    this.minAttendee = "minAttendee" in obj ? obj.minAttendee : 0;
+    this._minAttendee = "minAttendee" in obj ? obj.minAttendee : 0;
     this.maxAttendee = "maxAttendee" in obj ? obj.maxAttendee : 0;
-    this.benevoleIdList = "benevoleIdList" in obj ? obj.benevoleIdList : [];
+    this._benevoleIdList = "benevoleIdList" in obj ? obj.benevoleIdList : [];
     this.competencesIdList = "competencesIdList" in obj ? obj.competencesIdList : [];
+    this.updateEventContent();
   }
   get id(): string {
     return this.event.id;
@@ -48,15 +72,15 @@ class Creneau implements ICreneau {
   set ressourceId(value: string) {
     this.event.ressourceId = value;
   }
-  get title(): string {
-    return this.event.title;
-  }
-  set title(value: string) {
-    this.event.title = value;
+  get start(): Date {
+    return this.event.start;
   }
   set start(value: Date) {
     this.event.start = value;
   }
+  get end(): Date {
+    return this.event.end;
+  }
   set end(value: Date) {
     this.event.end = value;
   }
@@ -77,6 +101,11 @@ class Creneau implements ICreneau {
       dayjs(this.event.end).format("HH:mm")
     );
   }
+  updateEventContent() {
+    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>`;
+  }
   toPlainObject(): ICreneau {
     return {
       event: this.event,

+ 2 - 5
src/utils/Toast.ts

@@ -5,7 +5,7 @@ interface ToastOptions {
   inDuration: number;
   outDuration: number;
   classes: string;
-  completeCallback: (toast: Toast) => void;
+  completeCallback?: (toast: Toast) => void;
   activationPercent: number;
 }
 const defaultOptions: ToastOptions = {
@@ -14,9 +14,6 @@ const defaultOptions: ToastOptions = {
   inDuration: 300,
   outDuration: 375,
   classes: "",
-  completeCallback: (toast: Toast) => {
-    toast.update();
-  },
   activationPercent: 0.7,
 };
 class Toast {
@@ -128,7 +125,7 @@ class Toast {
       if (Toast._toasts.length === 0) {
         Toast._removeContainer();
       }
-      this.options.completeCallback(this);
+      if (this.options.completeCallback) this.options.completeCallback(this);
     }, this.options.outDuration);
   }
   static dismissAll() {

+ 176 - 25
src/views/Planning.vue

@@ -3,11 +3,36 @@
     <div class="timeline">
       <div class="timelime-control">
         <h1>Planning global de l'événement</h1>
-        <button class="btn primary"><i class="material-icons">upload_file</i></button>
-        <button class="btn primary"><i class="material-icons">save</i></button>
+        <div class="actions">
+          <button class="btn icon small primary">
+            <i class="material-icons">upload_file</i>
+          </button>
+          <button class="btn icon small primary"><i class="material-icons">save</i></button>
+          <button
+            class="btn small secondary"
+            @click="createCreneau"
+            :disabled="creneauGroupList.length == 0"
+          >
+            <i class="material-icons">create</i>Creneau
+          </button>
+          <button class="btn small secondary" @click="createRessource">
+            <i class="material-icons">create</i>Ligne
+          </button>
+          <button class="btn icon small primary" @click="zoom(1)">
+            <i class="material-icons">zoom_in</i>
+          </button>
+          <button class="btn icon small primary" @click="zoom(-1)">
+            <i class="material-icons">zoom_out</i>
+          </button>
+        </div>
       </div>
       <jc-timeline
         ref="timeline"
+        :slotduration="slotduration"
+        :legendspan="legendspan"
+        :slotwidth="slotwidth"
+        start="2021-04-04T22:00:00.000Z"
+        end="2021-04-06T21:59:00.000Z"
         @event-change="eventChangeHandler"
         @item-selected="selectionChangeHandler"
         @reorder-ressource="ressourceChangeHandler"
@@ -17,7 +42,11 @@
     <editeur-creneau
       v-if="currentCreneau"
       class="editor"
-      :creneauId="currentCreneau.id"
+      :creneau="currentCreneau"
+      @create="createCreneau"
+      @delete="deleteCreneau"
+      @duplicate="duplicateCreneau"
+      @edit="updateCreneau"
     ></editeur-creneau>
     <editeur-ligne
       v-else
@@ -43,7 +72,11 @@ import { Ressource } from "jc-timeline";
 import Creneau from "@/models/Creneau";
 import { MutationTypes } from "@/store/Mutations";
 import Selectable from "node_modules/jc-timeline/lib/utils/selectable";
+import toast from "@/utils/Toast";
 
+import * as dayjs from "dayjs";
+import "dayjs/locale/fr";
+dayjs.locale("fr");
 type changePayload = {
   items: Array<jcEvent>;
 };
@@ -60,15 +93,72 @@ export default defineComponent({
       endDate: "",
       currentCreneau: undefined as Creneau | undefined,
       currentCreneauGroup: undefined as Ressource | undefined,
+      zoomLevel: 14,
+      slotduration: 30,
+      legendspan: 2,
+      slotwidth: 30,
     };
   },
   methods: {
+    createCreneau(): void {
+      const groupId = this.currentCreneau
+        ? this.currentCreneau.ressourceId
+        : this.currentCreneauGroup
+        ? this.currentCreneauGroup.id
+        : false;
+      if (groupId) {
+        const start = new Date(this.timeline.start);
+        const e = this.timeline.addEvent(
+          new jcEvent({
+            id: uuidv4(),
+            start: start,
+            end: new Date(start.getTime() + 1000 * 60 * 60),
+            ressourceId: groupId,
+            title: "Nouveau creneau",
+          })
+        );
+        if (e) {
+          this.timeline.clearSelectedItems();
+          e.selected = true;
+          const creneau = new Creneau({
+            event: e,
+            penibility: 12,
+            minAttendee: 1,
+            maxAttendee: 1,
+            benevoleIdList: [],
+            competencesIdList: [],
+            description: "",
+          });
+          this.$store.commit(MutationTypes.addCreneau, creneau);
+
+          this.currentCreneau = this.$store.getters.getCreneauById(creneau.id);
+          this.currentCreneauGroup = undefined;
+        }
+      } else {
+        toast({
+          html:
+            "Erreur: Pas de ligne auquel associer un nouveau creneau.<br>" +
+            " Veuillez selectioné un créneau existant ou une ligne.",
+        });
+      }
+    },
     createRessource(): void {
       const ressource = new Ressource({ id: uuidv4(), title: "Nouvelle ligne" });
       this.$store.commit(MutationTypes.addCreneauGroup, ressource);
-      this.timeline.addRessource(ressource);
+      this.timeline.clearSelectedItems();
+      this.timeline.addRessource(ressource).selected = true;
       this.currentCreneauGroup = ressource;
     },
+    duplicateCreneau(payload: Creneau) {
+      const newCreneau = new Creneau({
+        ...payload.toPlainObject(),
+        event: new jcEvent({ ...payload.event, id: uuidv4() }),
+      });
+      newCreneau.title = "Copy de " + newCreneau.title;
+      this.timeline.addEvent(newCreneau.event);
+      this.$store.commit(MutationTypes.addCreneau, newCreneau);
+      this.currentCreneau = newCreneau;
+    },
     deleteRessource(): void {
       if (this.currentCreneauGroup) {
         const contentRemoved = this.timeline.removeRessourceById(this.currentCreneauGroup.id);
@@ -81,6 +171,12 @@ export default defineComponent({
         });
       }
     },
+    deleteCreneau(): void {
+      if (this.currentCreneau) {
+        this.timeline.removeEventById(this.currentCreneau.id);
+        this.$store.commit(MutationTypes.removeCreneau, this.currentCreneau);
+      }
+    },
     selectionChangeHandler(ev: CustomEvent) {
       const elts = ev.detail.items as Array<Selectable>;
       if (elts.length == 1) {
@@ -100,7 +196,9 @@ export default defineComponent({
       }
     },
     eventChangeHandler(ev: CustomEvent<changePayload>) {
+      this.currentCreneauGroup = undefined;
       const jcEvents = ev.detail.items;
+
       for (let index = 0; index < jcEvents.length; index++) {
         const element = jcEvents[index];
         this.$store.commit(MutationTypes.editCreneau, {
@@ -118,6 +216,7 @@ export default defineComponent({
           field: "ressourceId",
           value: element.ressourceId,
         });
+        this.currentCreneau = this.$store.getters.getCreneauById(element.id);
       }
     },
     ressourceChangeHandler(ev: CustomEvent<{ ressources: Array<Ressource> }>) {
@@ -128,28 +227,59 @@ export default defineComponent({
       field: K;
       value: Ressource[K];
     }) {
-      this.$store.commit(MutationTypes.editCreneauGroup, payload);
-      const newRessource = this.$store.getters.getCreneauGroupById(payload.id);
-      if (newRessource) {
-        if (payload.field == "parent") {
-          const content = this.timeline.removeRessourceById(payload.id);
-          this.timeline.addRessources(
-            content.ressources.map((o) => (o.id == payload.id ? newRessource : o))
-          );
-          this.timeline.addRessources(content.items);
-        } else {
-          this.timeline.updateLegend();
+      const prevState = this.$store.getters.getCreneauGroupById(payload.id);
+      if (prevState && prevState[payload.field] !== payload.value) {
+        const r = this.timeline.updateRessource(payload.id, payload.field, payload.value);
+        if (r) {
+          payload.value = r[payload.field];
         }
+        this.$store.commit(MutationTypes.editCreneauGroup, payload);
       }
     },
     updateCreneau<K extends keyof Creneau>(payload: { id: string; field: K; value: Creneau[K] }) {
       this.$store.commit(MutationTypes.editCreneau, payload);
-      this.timeline.updateEventById(payload.id);
+      const creneau = this.$store.getters.getCreneauById(payload.id);
+      if (creneau) {
+        this.timeline.removeEventById(payload.id);
+        this.timeline.addEvent(creneau.event);
+      }
+    },
+    zoom(lvl: number) {
+      const durations = [
+        10080,
+        10080,
+        10080,
+        10080,
+        10080,
+        1440,
+        1440,
+        720,
+        720,
+        360,
+        120,
+        60,
+        60,
+        60,
+        30,
+        30,
+        15,
+        5,
+      ];
+      const slotWidths = [40, 60, 80, 120, 160, 30, 40, 30, 50, 40, 25, 20, 30, 40, 30, 40, 35, 20];
+      const legendSpans = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 2, 2, 1, 4];
+      this.zoomLevel += lvl;
+      this.zoomLevel = Math.min(Math.max(0, this.zoomLevel), durations.length - 1);
+
+      this.slotduration = durations[this.zoomLevel];
+      this.slotwidth = slotWidths[this.zoomLevel];
+      this.legendspan = legendSpans[this.zoomLevel];
     },
   },
   computed: {
     timeline(): Timeline {
-      return this.$refs["timeline"] as Timeline;
+      const output = this.$refs["timeline"];
+      console.log(output);
+      return output as Timeline;
     },
     currentCreneauGroupId(): string {
       return this.currentCreneauGroup ? this.currentCreneauGroup.id : "";
@@ -164,17 +294,31 @@ export default defineComponent({
       return this.$store.state.creneauGroupList;
     },
   },
-  watch: {
-    creneauGroupList() {
-      this.timeline.requestUpdate();
-    },
-    creneauList() {
-      this.timeline.requestUpdate();
-    },
-  },
+  watch: {},
   mounted() {
     this.timeline.addRessources(this.creneauGroupList);
     this.timeline.addEvents(this.eventList);
+    this.timeline.setLegendUnitFormat("d", "dddd D MMMM");
+    this.timeline.customStyle = `.bubble{
+    height: 20px;
+    width: 20px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    right: -4px;
+    bottom: -4px;
+    border-radius: 50%;
+    font-size: 12px;
+    font-weight: bold;
+    background: green;
+}
+.bubble.red{
+    background: red;
+}
+.bubble.orange{
+    background: orange;
+}`;
   },
 });
 </script>
@@ -198,4 +342,11 @@ jc-timeline {
   box-shadow: 0 0 2px 2px var(--color-neutral-600);
   height: 100%;
 }
+.actions {
+  display: inline-flex;
+}
+
+.actions > button {
+  margin-right: 4px;
+}
 </style>