瀏覽代碼

implement benevoles import/export

tripeur 4 年之前
父節點
當前提交
24c8338d0b
共有 6 個文件被更改,包括 219 次插入21 次删除
  1. 4 6
      src/components/DataTable.vue
  2. 13 11
      src/components/input.vue
  3. 1 1
      src/models/Competence.ts
  4. 82 0
      src/utils/csv.ts
  5. 119 2
      src/views/BenevoleManager.vue
  6. 0 1
      src/views/Login.vue

+ 4 - 6
src/components/DataTable.vue

@@ -61,10 +61,8 @@
             :key="columnIndex"
             :class="[column.numeric ? 'numeric' : '', column.class]"
           >
-            <div v-if="!column.html">
-              {{ row[column.field] }}
-            </div>
             <div v-if="column.html" v-html="row[column.field]" />
+            <template v-else>{{ row[column.field] }}</template>
           </td>
           <slot name="tbody-tr" :row="row" />
         </tr>
@@ -334,10 +332,10 @@ export default defineComponent({
             .join(strSpeparator) + "\r\n";
       });
       const documentPrefix = this.title ? this.title.replace(/ /g, "-") : "Sheet";
-      const d = new Date();
+      const d = dayjs();
       const dummy = document.createElement("a");
       dummy.href = mimeType + ", " + encodeURI(csvContent);
-      dummy.download = documentPrefix + dayjs(d).format("-YYYY-MM-DD-hh-mm-ss[.csv]");
+      dummy.download = documentPrefix + d.format("-YYYY-MM-DD-hh-mm-ss[.csv]");
       document.body.appendChild(dummy);
       dummy.click();
     },
@@ -741,7 +739,7 @@ td.fitwidth {
   max-width: 70px;
 }
 
-.overflow-cell div {
+.overflow-cell > * {
   max-width: 100%;
   white-space: nowrap;
   overflow: hidden;

+ 13 - 11
src/components/input.vue

@@ -80,12 +80,11 @@ export default defineComponent({
     },
     disabled: { type: Boolean, default: false },
   },
-  emits: ["update:modelValue"],
   computed: {
-    inputScore: function (): number {
+    inputScore(): number {
       return this.validateInput(this.value);
     },
-    controlClass: function (): string {
+    controlClass(): string {
       if (this.inputScore < 0) {
         return "formcontrol--error";
       }
@@ -94,7 +93,7 @@ export default defineComponent({
       }
       return "";
     },
-    inputClass: function (): string {
+    inputClass(): string {
       if (this.inputScore < 0) {
         return "input--invalid";
       }
@@ -105,8 +104,9 @@ export default defineComponent({
     },
   },
   watch: {
-    modelValue: function (val: string): void {
+    modelValue(val: string): void {
       this.value = val;
+      this.resizetxtArea();
     },
     value(new_val: string): void {
       if (new_val !== this.modelValue) this.$emit("update:modelValue", new_val);
@@ -114,16 +114,18 @@ export default defineComponent({
   },
   methods: {
     resizetxtArea() {
-      const a = this.$refs.textarea as HTMLElement;
-      a.style.height = "auto";
-      a.style.height = a.scrollHeight + "px";
+      if (this.type == "textarea") {
+        this.$nextTick(() => {
+          const a = this.$refs.textarea as HTMLElement;
+          a.style.height = "auto";
+          a.style.height = a.scrollHeight + "px";
+        });
+      }
     },
   },
   mounted(): void {
     this.value = this.modelValue + "";
-    if (this.type == "textarea") {
-      this.resizetxtArea();
-    }
+    this.resizetxtArea();
   },
 });
 </script>

+ 1 - 1
src/models/Competence.ts

@@ -53,8 +53,8 @@ export default class Competence {
     return this.isPreference ? 1 : this.isTeachable ? 2 : 3;
   }
   get overflowDescription(): string {
-    // TODO implement tooltiped description
     return this.description;
+    // return `<div class="tooltiped tooltiped--medium" aria-tooltip="${this.description}" >${this.description}</div>`;
   }
   get fullname(): string {
     return this.name + (this.isPreference ? " (P)" : " (C)");

+ 82 - 0
src/utils/csv.ts

@@ -0,0 +1,82 @@
+enum csvState {
+  parseValue,
+  parseDelimitedValue,
+  parseEntry,
+}
+type csvOptions = {
+  delimiter: string;
+  separator: string;
+};
+const defaultoptions: csvOptions = {
+  delimiter: '"',
+  separator: ",",
+};
+const parseCSV = function (str: string, options: Partial<csvOptions> = defaultoptions) {
+  const o = options ? { ...options, ...defaultoptions } : defaultoptions;
+  const delimiter = o.delimiter;
+  const separator = o.separator;
+  const output = [];
+  let currentValue = "";
+  let currentEntry = [];
+  let state: csvState = csvState.parseValue;
+  let i = 0;
+  while (i < str.length) {
+    const c = str[i];
+    if (state == csvState.parseEntry) {
+      if (c == delimiter) {
+        state = csvState.parseDelimitedValue;
+        i++;
+      } else if (c == separator) {
+        i++;
+        state = csvState.parseValue;
+      } else if (c == "\r" || c == "\n") {
+        output.push(currentEntry);
+        currentEntry = [];
+        i += str[i + 1] == "\n" ? 2 : 1;
+      } else {
+        state = csvState.parseValue;
+      }
+    } else if (state == csvState.parseValue) {
+      if (c == separator || c == "\r" || c == "\n") {
+        currentEntry.push(currentValue);
+        currentValue = "";
+        state = csvState.parseEntry;
+      } else {
+        currentValue += c;
+        i++;
+      }
+    } else if (state == csvState.parseDelimitedValue) {
+      if (c == delimiter) {
+        if (str[i + 1] == delimiter) {
+          currentValue += delimiter;
+          i++;
+        } else {
+          currentEntry.push(currentValue);
+          currentValue = "";
+          state = csvState.parseEntry;
+        }
+      } else {
+        currentValue += c;
+      }
+      i++;
+    }
+  }
+  return output;
+};
+const toObject = function (str: string, options: Partial<csvOptions> = defaultoptions) {
+  const arr = parseCSV(str, options);
+  return ArraytoObject(arr);
+};
+const ArraytoObject = function (arr: Array<Array<string>>) {
+  const output = [];
+  const header = arr[0].map((s) => s.trim());
+  for (let i = 1; i < arr.length; i++) {
+    const obj: { [k: string]: string } = {};
+    for (let j = 0; j < header.length; j++) {
+      obj[header[j]] = arr[i][j];
+    }
+    output.push(obj);
+  }
+  return output;
+};
+export default { toObject, toArray: parseCSV, ArraytoObject };

+ 119 - 2
src/views/BenevoleManager.vue

@@ -6,28 +6,39 @@
       :data="data"
       :clickable="true"
       :loadingAnimation="loading"
+      :exportable="false"
       locale="fr"
       @row-click="onRowClick"
+      :customButtons="customButton"
     ></data-table>
     <editeur-benevole
       :benevole="currentBenevole"
       @create="createBenevole"
       @delete="deleteBenevole"
     />
+    <input type="file" ref="loadcsv" @change="importBenevoleTemplate" style="display: none" />
   </div>
 </template>
 
 <script lang="ts">
 import Competence from "@/models/Competence";
 import EditeurBenevole from "@/components/EditeurBenevole.vue";
-import DataTable, { DataTableData, DataTableObject } from "@/components/DataTable.vue";
+import DataTable, {
+  CustomButton,
+  DataTableData,
+  DataTableObject,
+} from "@/components/DataTable.vue";
 import { defineComponent } from "vue";
 import { MutationTypes } from "@/store/Mutations";
 import Benevole from "@/models/Benevole";
+import csv from "@/utils/csv";
+import dayjs from "dayjs";
+import Toast from "@/utils/Toast";
 
+const csvDefaultcolumn = "id,prenom,nom,telephone,email,commentaire";
 export default defineComponent({
   name: "EditeurCreneau",
-  components: { "data-table": DataTable, EditeurBenevole },
+  components: { DataTable, EditeurBenevole },
   data: () => ({
     columns: [
       {
@@ -70,8 +81,12 @@ export default defineComponent({
     ],
     currentBenevole: undefined as Benevole | undefined,
     loading: true,
+    customButton: [] as Array<CustomButton>,
   }),
   computed: {
+    csvInputElt(): HTMLInputElement {
+      return this.$refs.loadcsv as HTMLInputElement;
+    },
     competenceList(): Array<Competence> {
       return this.$store.state.competenceList;
     },
@@ -125,9 +140,111 @@ export default defineComponent({
     onRowClick(row: { id: number }) {
       this.currentBenevole = this.$store.getters.getBenevoleById(row.id);
     },
+    getImportTemplate() {
+      const competences = this.$store.state.competenceList;
+      let csvContent = `${csvDefaultcolumn},${competences.map((c) => c.name).join(",")}\r\n`;
+      this.benevoleList.forEach(
+        (b) =>
+          (csvContent +=
+            [
+              b.id,
+              b.name,
+              b.surname,
+              b.phone,
+              b.email,
+              b.comment,
+              ...competences.map((c) => (b.competenceIdList.includes(c.id) ? "X" : "")),
+            ].join(",") + "\r\n")
+      );
+      const dummy = document.createElement("a");
+      dummy.href = "data:text/csv;charset=utf-8, " + encodeURI(csvContent);
+      dummy.download = "Benevole-import" + dayjs().format("-YYYY-MM-DD-hh-mm-ss[.csv]");
+      document.body.appendChild(dummy);
+      dummy.click();
+    },
+    importBenevoleTemplate() {
+      const fs = this.csvInputElt.files;
+      if (fs && fs.length > 0) {
+        let reader = new FileReader();
+        reader.readAsText(fs[0]);
+        reader.onload = () => {
+          let csvtxt = reader.result as string;
+          const arr = csv.toArray(csvtxt);
+          const defaultKey = csvDefaultcolumn.split(",");
+          const competencesMap: { [k: string]: Competence } = {};
+          for (let key of arr[0]) {
+            if (!defaultKey.includes(key)) {
+              key = key.trim();
+              // check if a competence exist with this name
+              let c = this.competenceList.find((c) => c.name == key);
+              if (c == undefined) {
+                Toast({ html: "Compétence inconnue: " + key, classes: "warning" });
+                c = Competence.fromObject({ name: key, description: "" });
+                this.$store.commit(MutationTypes.addConstraint, c);
+              }
+              if (c) {
+                competencesMap[key] = c;
+              }
+            }
+          }
+          const objectList = csv.toObject(csvtxt);
+          for (let item of objectList) {
+            const id = parseInt(item.id);
+
+            let b: Benevole | undefined = undefined;
+            if (id) b = this.$store.getters.getBenevoleById(id);
+            if (b == undefined) {
+              b = Benevole.fromObject({
+                id: id ?? undefined,
+                name: item.prenom,
+                surname: item.nom,
+                phone: item.telephone,
+                email: item.email,
+                comment: item.commentaire,
+              });
+              this.$store.commit(MutationTypes.addBenevole, b);
+            } else {
+              let map: Array<{ k: string; v: keyof Benevole }> = [
+                { k: "prenom", v: "name" },
+                { k: "nom", v: "surname" },
+                { k: "telephone", v: "phone" },
+                { k: "commentaire", v: "comment" },
+                { k: "email", v: "email" },
+              ];
+              map.forEach((o) =>
+                this.$store.commit(MutationTypes.editBenevole, {
+                  id: (b as Benevole).id,
+                  field: o.v,
+                  value: item[o.k],
+                })
+              );
+            }
+            const competenceIdList = [];
+            for (let key in competencesMap) {
+              if (item[key].trim() == "X") {
+                competenceIdList.push(competencesMap[key].id);
+              }
+            }
+            this.$store.commit(MutationTypes.editBenevole, {
+              id: b.id,
+              field: "competenceIdList",
+              value: competenceIdList,
+            });
+          }
+        };
+      }
+    },
   },
   mounted() {
     this.loading = false;
+    this.customButton.push({ icon: "file_download", hide: false, onclick: this.getImportTemplate });
+    this.customButton.push({
+      icon: "file_upload",
+      hide: false,
+      onclick: () => {
+        this.csvInputElt.click();
+      },
+    });
   },
 });
 </script>

+ 0 - 1
src/views/Login.vue

@@ -68,7 +68,6 @@ export default defineComponent({
         Toast({ html: "Les mot de passe ne correspondent pas.", classes: "error" });
         this.error = true;
       }
-      // todo send a request to the server and not answer it
     },
     checkPassword(str: string) {
       return this.password == str;