瀏覽代碼

implement user manager

tripeur 4 年之前
父節點
當前提交
db10d01216

+ 4 - 4
src/LoginPage.vue

@@ -1,6 +1,6 @@
 <template>
   <c-header title="BDLG Planner" titleTo="/planner" />
-  <div class="container">
+  <div class="login-container">
     <login class="login-box" />
   </div>
 </template>
@@ -14,8 +14,8 @@ export default defineComponent({
   components: { login, cHeader },
 });
 </script>
-<style>
-.container {
+<style scoped>
+.login-container {
   width: 100%;
   display: flex;
   justify-content: center;
@@ -32,7 +32,7 @@ export default defineComponent({
     padding: 8px 32px 16px;
     min-width: 420px;
   }
-  .container {
+  .login-container {
     max-height: 500px;
   }
 }

+ 0 - 1
src/PlannerApp.vue

@@ -68,7 +68,6 @@ export default defineComponent({
   },
   mounted() {
     const url = `${API_URL}api/evenements`;
-    console.log(url);
     fetch(url)
       .then((response) => {
         if (response.status == 200) {

+ 1 - 8
src/PlanningPage.vue

@@ -1,6 +1,6 @@
 <template>
   <c-header title="BDLG Planner" titleTo="/planner" />
-  <div class="container">
+  <div class="main-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>
@@ -66,13 +66,6 @@ export default defineComponent({
 </script>
 
 <style scoped>
-.container {
-  display: flex;
-  justify-content: center;
-  flex-direction: column;
-  margin: 8px;
-}
-
 .no-info {
   display: flex;
   flex-direction: column;

+ 3 - 2
src/components/DataTable.vue

@@ -7,7 +7,7 @@
           v-for="(button, index) in shownCustomButtons"
           :key="index"
           href="javascript:undefined"
-          class="btn small secondary"
+          class="btn icon small secondary"
           @click="button.onclick"
         >
           <i class="material-icons">{{ button.icon }}</i>
@@ -124,6 +124,7 @@
 
 <script lang="ts">
 import { defineComponent, PropType } from "vue";
+import "@/assets/css/button.css";
 import Fuse from "fuse.js";
 import { Dictionary } from "lodash";
 import dayjs from "dayjs";
@@ -151,7 +152,7 @@ var locales: Dictionary<DataTableLocale> = {
     empty: "Pas de donnée à afficher",
   },
 };
-interface CustomButton {
+export interface CustomButton {
   icon: string;
   hide: boolean;
   onclick: (e: MouseEvent) => void;

+ 1 - 1
src/components/EvenementDataTable.vue

@@ -5,7 +5,7 @@
         <th>Nom</th>
         <th>Modifié le</th>
         <th>Par</th>
-        <th>Charger</th>
+        <th>Action</th>
       </tr>
     </thead>
     <tbody>

+ 2 - 2
src/components/Footer.vue

@@ -12,8 +12,8 @@ const API_URL = process.env.VUE_APP_API_URL;
 export default defineComponent({
   data() {
     return {
-      roles: ["ADMIN"] as Array<string>,
-      connected: true,
+      roles: [] as Array<string>,
+      connected: false,
       logout: API_URL + "logout",
       adminPage: API_URL + "admin",
       year: new Date().getFullYear() + "",

+ 0 - 1
src/components/date-picker.vue

@@ -328,7 +328,6 @@ export default defineComponent({
 </script>
 <style>
 .date-input-control {
-  width: 8rem;
   margin-right: 4px;
   position: relative;
 }

+ 12 - 0
src/models/MyUser.ts

@@ -0,0 +1,12 @@
+export type Role = {
+  id: number;
+  name: string;
+};
+type MyUser = {
+  id?: number;
+  username: string;
+  email: string;
+  password?: string;
+  roles?: Array<Role>;
+};
+export default MyUser;

+ 11 - 1
src/views/Admin.vue

@@ -1,5 +1,15 @@
 <template>
-  <div></div>
+  <div>
+    <h3>Bienvenue sur le site d'adminitration de BDLG planner</h3>
+    <ul>
+      <li>
+        <router-link to="/admin/users">Gestion des utilisateurs</router-link>
+      </li>
+      <li>
+        <router-link to="/admin/evenements">Gestion des évenements</router-link>
+      </li>
+    </ul>
+  </div>
 </template>
 
 <script lang="ts">

+ 1 - 1
src/views/CompetenceManager.vue

@@ -25,7 +25,7 @@ import { defineComponent } from "vue";
 import { MutationTypes } from "@/store/Mutations";
 
 export default defineComponent({
-  name: "EditeurCreneau",
+  name: "CompetenceManager",
   components: { "data-table": DataTable, EditeurContrainte },
   data: () => ({
     columns: [

+ 103 - 3
src/views/EvenementManager.vue

@@ -1,13 +1,113 @@
 <template>
-  <div></div>
+  <div style="width: 95%; max-width: 900px">
+    <data-table
+      title="Utilisateurs"
+      :data="data"
+      :searchable="true"
+      :customButtons="customButtons"
+      :clickable="false"
+      :loadingAnimation="isLoading"
+    >
+      <template v-slot:thead-tr> <th style="width: 100px">Action</th></template>
+      <template v-slot:tbody-tr="props">
+        <td class="fitwidth">
+          <button class="btn icon small error" @click="deleteEvt(props.row.uuid)">
+            <i class="material-icons">delete_forever</i>
+          </button>
+        </td></template
+      >
+    </data-table>
+  </div>
 </template>
 
 <script lang="ts">
 import { defineComponent } from "vue";
+import DataTable, { DataTableData, DataTableObject } from "@/components/DataTable.vue";
+import EvenementVersion from "@/models/EvenementVersion";
+import dayjs from "dayjs";
+import Toast from "@/utils/Toast";
+
+const API_URL = process.env.VUE_APP_API_URL;
 
 export default defineComponent({
-  setup() {
-    return {};
+  components: { DataTable },
+  data() {
+    return {
+      evtList: [] as Array<EvenementVersion>,
+      columns: [
+        {
+          label: "Nom",
+          field: "name",
+          numeric: false,
+          html: false,
+          class: "overflow-cell",
+        },
+        {
+          label: "Modifié le",
+          field: "modified",
+          numeric: false,
+          html: false,
+          width: 190,
+          class: "fitwidth",
+        },
+        {
+          label: "Par",
+          field: "username",
+          numeric: false,
+          html: false,
+          class: "overflow-cell",
+        },
+      ],
+      isLoading: true,
+    };
+  },
+  computed: {
+    data(): DataTableData<DataTableObject> {
+      return {
+        items: this.evtList.map((evt) => {
+          return {
+            uuid: evt.uuid,
+            name: evt.name,
+            modified: dayjs(evt.lastModified).format("DD MMM YYYY à HH[h]mm"),
+            username: evt.lastEditor.username,
+          };
+        }),
+        columns: this.columns,
+      };
+    },
+  },
+  methods: {
+    deleteEvt(uuid: string) {
+      const evt = this.evtList.find((e) => e.uuid == uuid);
+      if (
+        evt &&
+        confirm(
+          "Êtes vous sûr de vouloir supprimer toutes les donénes relatives à l'événement " +
+            evt.name
+        )
+      )
+        fetch(`${API_URL}api/evenements/${uuid}`, { method: "DELETE" }).then((res) => {
+          if (res.status == 200) {
+            this.evtList = this.evtList.filter((e) => e.uuid != uuid);
+            Toast({ html: "L'évenement a été supprimé <br>" + evt.name, classes: "success" });
+          } else {
+            Toast({ html: "L'évenement n'a pas pu être supprimé: " + evt.name, classes: "error" });
+          }
+        });
+    },
+  },
+  mounted() {
+    const url = `${API_URL}api/evenements`;
+    fetch(url)
+      .then((response) => {
+        if (response.status == 200) {
+          this.isLoading = false;
+          return response.json();
+        } else {
+          throw new Error(response.statusText);
+        }
+      })
+      .then((data) => (this.evtList = data));
   },
 });
 </script>

+ 1 - 0
src/views/Login.vue

@@ -66,6 +66,7 @@ export default defineComponent({
     fetchRegister() {
       if (this.password != this.passwordCheck) {
         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
     },

+ 289 - 3
src/views/UserManager.vue

@@ -1,15 +1,301 @@
 <template>
-  <div></div>
+  <div style="width: 95%; max-width: 900px">
+    <data-table
+      title="Utilisateurs"
+      :data="data"
+      :searchable="true"
+      :customButtons="customButtons"
+      :clickable="false"
+      :loadingAnimation="isLoading"
+    >
+      <template v-slot:thead-tr> <th style="width: 100px">Action</th></template>
+      <template v-slot:tbody-tr="props">
+        <td class="fitwidth">
+          <button class="btn icon small primary" @click="editUser(props.row.username)">
+            <i class="material-icons">edit</i></button
+          >&nbsp;
+          <button class="btn icon small error" @click="deleteUser(props.row.username)">
+            <i class="material-icons">delete_forever</i>
+          </button>
+        </td></template
+      >
+    </data-table>
+
+    <div v-if="showModal" class="modal" @click="closeModal">
+      <div class="modal-content user-form" ref="modal">
+        <h3 v-if="mode == 'edition'">Modifier Utilisateur</h3>
+        <h3 v-if="mode == 'creation'">Nouvel Utilisateur</h3>
+        <styled-input
+          label="Nom d'utilisateur"
+          v-model="userName"
+          :validateInput="usernameIncorrect"
+          :disabled="mode == 'edition'"
+        />
+        <styled-input label="Adresse email" v-model="email" />
+        <styled-input label="Mot de Passe" v-model="pwd" type="password" />
+        <styled-input
+          label="Confirmation du mot de Passe"
+          v-model="pwdcheck"
+          :validateInput="passwordIncorrect"
+          type="password"
+        />
+        <chips-input
+          id="preference_selection"
+          label="Rôles"
+          placeholder="Choisir un rôle"
+          :autocompleteList="autocompleteList"
+          :strict-autocomplete="true"
+          v-model="roleList"
+        />
+        <div class="actions" style="margin-top: 8px; text-align: right">
+          <button class="btn primary small" @click="modifyUser" v-if="mode == 'edition'">
+            <i class="material-icons">save</i>Sauvegarder
+          </button>
+          <button class="btn primary small" @click="createUser" v-if="mode == 'creation'">
+            <i class="material-icons">create</i>Créer
+          </button>
+          <button class="btn error small" @click="showModal = false">
+            <i class="material-icons">close</i>Annuler
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script lang="ts">
+import Toast from "@/utils/Toast";
+import User, { Role } from "@/models/MyUser";
 import { defineComponent } from "vue";
+import styledInput from "../components/input.vue";
+import chipsInput from "../components/SelectChipInput.vue";
+import DataTable, {
+  CustomButton,
+  DataTableData,
+  DataTableObject,
+} from "@/components/DataTable.vue";
 
+const API_URL = process.env.VUE_APP_API_URL;
 export default defineComponent({
+  components: { styledInput, chipsInput, DataTable },
   data() {
-    return {};
+    return {
+      columns: [
+        {
+          label: "Nom",
+          field: "username",
+          numeric: false,
+          html: false,
+          class: "overflow-cell",
+        },
+        {
+          label: "Email",
+          field: "email",
+          numeric: false,
+          html: false,
+          class: "overflow-cell",
+        },
+        {
+          label: "Rôles",
+          field: "rolesStr",
+          numeric: false,
+          html: false,
+          class: "fitwidth",
+        },
+      ],
+      customButtons: [] as Array<CustomButton>,
+      userList: [] as Array<User>,
+      potentialRoles: [] as Array<Role>,
+      roleList: [] as Array<string>,
+      showModal: false,
+      userName: "",
+      email: "",
+      pwd: "",
+      pwdcheck: "",
+      error: false,
+      errorUsername: false,
+      mode: "creation",
+      isLoading: true,
+    };
+  },
+  computed: {
+    passwordIncorrect(): () => number {
+      return this.error ? () => -1 : () => 0;
+    },
+    usernameIncorrect(): () => number {
+      return this.errorUsername ? () => -1 : () => 0;
+    },
+    autocompleteList(): Array<string> {
+      return this.potentialRoles.map((p) => p.name);
+    },
+    data(): DataTableData<DataTableObject> {
+      return {
+        items: this.userList.map((user) => {
+          return {
+            username: user.username,
+            email: user.email,
+            rolesStr: user.roles?.map((r) => r.name).join(", ") ?? "",
+          };
+        }),
+        columns: this.columns,
+      };
+    },
+  },
+  methods: {
+    resetForm() {
+      this.errorUsername = false;
+      this.error = false;
+      this.userName = "";
+      this.email = "";
+      this.pwd = "";
+      this.pwdcheck = "";
+      this.roleList = [];
+    },
+
+    editUser(username: string) {
+      this.resetForm();
+      const u = this.getUser(username);
+      this.userName = u.username;
+      this.email = u.email ?? "";
+      this.roleList = u.roles?.map((r) => r.name) ?? [];
+
+      this.mode = "edition";
+      this.showModal = true;
+    },
+    newUser() {
+      if (this.mode != "creation") {
+        this.resetForm();
+        this.mode = "creation";
+      }
+      this.showModal = true;
+    },
+    getUser(username: string): User {
+      return this.userList.find((user) => user.username == username) as User;
+    },
+    getFormUser(): User | null {
+      this.error = this.pwd !== this.pwdcheck;
+      if (this.error) {
+        Toast({ html: "Les mots de passe ne correspondent pas." });
+        return null;
+      } else {
+        return {
+          username: this.userName,
+          email: this.email,
+          password: this.pwd,
+          roles: this.roleList.map((name) => ({
+            name,
+            id: this.potentialRoles.find((r) => r.name == name)?.id as number,
+          })),
+        };
+      }
+    },
+    closeModal(e: MouseEvent) {
+      if (!(this.$refs.modal as HTMLElement).contains(e.target as HTMLElement)) {
+        this.showModal = false;
+      }
+    },
+    modifyUser() {
+      const user = this.getFormUser();
+      if (user) {
+        const toast = Toast({ html: "Modification de l'utilisateur: " + this.userName });
+        const url = `${API_URL}users/${this.pwd != "" ? "changePassword/" : ""}${this.userName}`;
+        fetch(url, {
+          method: "PUT",
+          headers: { "Content-Type": "application/json" },
+          body: JSON.stringify(user),
+        })
+          .then((res) => {
+            toast.dismiss();
+            if (res.status == 200) {
+              this.errorUsername = true;
+              return res.json();
+            }
+            throw new Error(res.statusText);
+          })
+          .then((data: User) => {
+            const idx = this.userList.findIndex((u) => u.username == data.username);
+            if (idx > -1) {
+              this.userList[idx] = data;
+            }
+            this.showModal = false;
+          })
+          .catch((err) => Toast({ html: err, classes: "error" }));
+      }
+    },
+    createUser() {
+      const user = this.getFormUser();
+      if (user) {
+        const toast = Toast({ html: "Création de l'utilisateur: " + this.userName });
+        fetch(API_URL + "users", {
+          method: "POST",
+          headers: { "Content-Type": "application/json" },
+          body: JSON.stringify(user),
+        })
+          .then((res) => {
+            toast.dismiss();
+            if (res.status == 200) {
+              this.errorUsername = false;
+              return res.json();
+            }
+            if (res.status == 409) {
+              Toast({ html: "Le nom d'utilisateur est déja utilisé", classes: "error" });
+              this.errorUsername = true;
+            }
+            if (res.status == 404) {
+              Toast({ html: "Le serveur ne répond pas.", classes: "error" });
+            }
+            throw new Error(res.statusText);
+          })
+          .then((data: User) => {
+            this.userList.push(data);
+            this.resetForm();
+            this.showModal = false;
+          })
+          .catch((err) => Toast({ html: err, classes: "error" }));
+      }
+    },
+    deleteUser(username: string) {
+      if (confirm("Êtes vous sûr de vouloir supprimer l'utilisateur " + username))
+        fetch(API_URL + "users/" + username, { method: "DELETE" }).then((res) => {
+          if (res.status == 200) {
+            Toast({ html: "Utilisateur supprimé : " + username, classes: "success" });
+            this.userList = this.userList.filter((u) => u.username != username);
+          } else {
+            Toast({ html: "L'utilisateur n'a pas été supprimé : " + username, classes: "error" });
+          }
+        });
+    },
+  },
+  mounted() {
+    fetch(API_URL + "roles")
+      .then((res) => {
+        if (res.status == 200) {
+          return res.json();
+        }
+        Toast({ html: "/roles " + res.statusText });
+      })
+      .then((data: unknown) => {
+        this.potentialRoles = data as Array<Role>;
+      });
+    fetch(API_URL + "users")
+      .then((res) => {
+        if (res.status == 200) {
+          return res.json();
+        }
+        Toast({ html: "/users " + res.statusText });
+      })
+      .then((data: unknown) => {
+        this.userList = data as Array<User>;
+        this.isLoading = false;
+      });
+    this.customButtons.push({ icon: "add", hide: false, onclick: this.newUser });
   },
 });
 </script>
 
-<style scoped></style>
+<style scoped>
+.user-form {
+  width: 95%;
+  max-width: 600px;
+}
+</style>