Browse Source

#13269
#13271
#13277
Enhance CSMS

vbea 2 năm trước cách đây
mục cha
commit
a108a65ce4

+ 388 - 0
Strides-Admin/src/components/DialogAssignment.vue

@@ -0,0 +1,388 @@
+<template>
+  <el-dialog
+    :title="title"
+    :visible="visible"
+    :before-close="onHide"
+    custom-class="assign-dialog">
+    <div class="filter-container filter-view">
+      <el-select
+        style="min-width: 70px; max-width: 120px;"
+        clearable
+        v-model="filter.pageVo.assignmentStatus"
+        placeholder="Status"
+        @change="onSearch">
+        <el-option
+          v-for="(item, index) in statusOptions"
+          :key="index"
+          :label="item"
+          :value="item"/>
+      </el-select>
+      <div style="flex: 1; min-width: 150px; max-width: 300px;">
+        <el-input
+          clearable
+          v-model="filter.pageVo.criteria"
+          placeholder="Search by Site Name or Service Provider"
+          @keyup.enter.native="onSearch"/>
+      </div>
+      <el-button
+        type="primary"
+        @click="onSearch">
+        Search
+      </el-button>
+    </div>
+    <div class="table-actions" v-if="isGroupAssign || isDyRateAssign">
+      <el-button
+        type="danger"
+        :disabled="selectRow.length == 0"
+        :loading="loading.unassign"
+        @click="onClickUnassign">
+        Batch Un-assign
+      </el-button>
+      <el-button
+        type="accent"
+        :disabled="selectRow.length == 0"
+        :loading="loading.assign"
+        @click="onClickAssign">
+        Batch Assign
+      </el-button>
+    </div>
+    <div class="table-view" v-loading="table.loading">
+      <el-table
+        :data="table.data"
+        height="100%"
+        class="no-border"
+        @selection-change="changeSelection">
+        <el-table-column
+          align="center"
+          label="Site Name"
+          prop="siteName"
+          min-width="120"/>
+        <el-table-column
+          align="center"
+          label="Address"
+          prop="address"
+          min-width="120"/>
+        <el-table-column
+          align="center"
+          label="Service Provider"
+          min-width="120">
+          <template slot-scope="{row}">
+            <div v-for="item in row.serviceProviders" :key="item">{{item}}</div>
+          </template>
+        </el-table-column>
+        <el-table-column
+          align="center"
+          label="Assignment Status"
+          prop="assignmentStatus"
+          min-width="120"/>
+        <el-table-column
+          align="center"
+          label="Select"
+          type="selection"/>
+      </el-table>
+    </div>
+    <div class="center" style="margin-bottom: -20px;">
+      <Pagination
+        v-show="table.total"
+        :total="table.total"
+        :page.sync="filter.pageNo"
+        :limit.sync="filter.pageSize"
+        @pagination="getTableData"/>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import apiGroup from '@/http/api/group'
+import apiRates from '@/http/api/rates'
+import Pagination from '@/components/Pagination'
+export default {
+  name: "DialogAssignment",
+  props: {
+    title: {
+      type: String,
+      default: "ASSIGN SITES"
+    },
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    group: {
+      type: Object,
+      default: () => ({})
+    },
+    rate: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  components: {Pagination},
+  data() {
+    return {
+      filter: {
+        pageSize: 10,
+        pageNo: 1,
+        pageVo: {
+          criteria: "",
+          countryCode: "",
+          assignmentStatus: ""
+        }
+      },
+      table: {
+        data: [],
+        total: 0,
+        loading: false
+      },
+      loading: {
+        assign: false,
+        unassign: false
+      },
+      statusOptions: [],
+      selectRow: [],
+      isGroupAssign: false,
+      isDyRateAssign: false
+    }
+  },
+  mounted() {
+    this.getStatusOptions();
+    //this.onSearch();
+  },
+  watch: {
+    visible: {
+      handler(n, o) {
+        console.log("watch.visible", n, o);
+        if (n) {
+          if (this.group && this.group.groupPk) {
+            this.isGroupAssign = true;
+            this.isDyRateAssign = false;
+            this.filter.pageVo.groupPk = this.group.groupPk
+            this.filter.pageVo.countryCode = this.group.countryCode
+            this.onSearch()
+          } else if (this.rate && this.rate.dynamicRateId) {
+            this.isGroupAssign = false;
+            this.isDyRateAssign = true;
+            this.filter.pageVo.countryCode = this.rate.countryCode
+            this.filter.pageVo.dynamicRateId = this.rate.dynamicRateId
+            this.onSearch()
+          }
+        }
+      }
+    }
+  },
+  methods: {
+    onHide() {
+      this.$emit("hide");
+    },
+    onSearch() {
+      this.filter.pageNo = 1;
+      this.getTableData();
+    },
+    getStatusOptions() {
+      apiGroup.getAssignStatusOptions().then(res => {
+        if (res.data) {
+          this.statusOptions = res.data
+        }
+      }).catch(error => {
+        this.$message({
+          type: 'error',
+          message: error
+        })
+      })
+    },
+    getTableData() {
+      this.selectRow = []
+      this.table.loading = true;
+      const promise = this.isGroupAssign 
+        ? apiGroup.getAssignSitesPages(this.filter)
+        : apiRates.getAssignSitesPages(this.filter)
+      promise.then(res => {
+        if (res.total && res.data) {
+          this.table.total = res.total;
+          this.table.data = res.data;
+        } else {
+          this.table.total = 0;
+          this.table.data = [];
+        }
+        this.table.loading = false;
+      }).catch(error => {
+        this.$message({
+          type: 'error',
+          message: error
+        })
+        this.table.total = 0;
+        this.table.data = [];
+        this.table.loading = false;
+      })
+    },
+    changeSelection(val) {
+      this.selectRow = val;
+    },
+    getSelectIds() {
+      const ids = [];
+      this.selectRow.forEach(item => {
+        ids.push(item.sitePk)
+      })
+      return ids;
+    },
+    onClickAssign() {
+      if (this.isGroupAssign) {
+        this.assignSites();
+      } else if (this.isDyRateAssign) {
+        this.assignRates();
+      }
+    },
+    onClickUnassign() {
+      if (this.isGroupAssign) {
+        this.unassignSites();
+      } else if (this.isDyRateAssign) {
+        this.unassignRates();
+      }
+    },
+    assignSites() {
+      const params = {
+        groupPk: this.group.groupPk,
+        sitePks: this.getSelectIds()
+      }
+      this.loading.assign = true;
+      apiGroup.assignSite2Group(params).then(res => {
+        this.$message({
+          type: 'success',
+          message: res.msg || "Success"
+        })
+        this.getTableData()
+      }).catch(error => {
+        this.$message({
+          type: 'error',
+          message: error
+        })
+      }).finally(() => {
+        this.loading.assign = false;
+      })
+    },
+    unassignSites() {
+      const params = {
+        groupPk: this.group.groupPk,
+        sitePks: this.getSelectIds()
+      }
+      this.loading.unassign = true;
+      apiGroup.unassignSite2Group(params).then(res => {
+        this.$message({
+          type: 'success',
+          message: res.msg || "Success"
+        })
+        this.getTableData()
+      }).catch(error => {
+        this.$message({
+          type: 'error',
+          message: error
+        })
+      }).finally(() => {
+        this.loading.unassign = false;
+      })
+    },
+    assignRates() {
+      const params = {
+        dynamicRateId: this.rate.dynamicRateId,
+        sitePks: this.getSelectIds()
+      }
+      this.loading.assign = true;
+      apiRates.assignRate2Site(params).then(res => {
+        this.$message({
+          type: 'success',
+          message: res.msg || "Success"
+        })
+        this.getTableData()
+      }).catch(error => {
+        this.$message({
+          type: 'error',
+          message: error
+        })
+      }).finally(() => {
+        this.loading.assign = false;
+      })
+    },
+    unassignRates() {
+      const params = {
+        dynamicRateId: this.rate.dynamicRateId,
+        sitePks: this.getSelectIds()
+      }
+      this.loading.unassign = true;
+      apiRates.unassignRate2Site(params).then(res => {
+        this.$message({
+          type: 'success',
+          message: res.msg || "Success"
+        })
+        this.getTableData()
+      }).catch(error => {
+        this.$message({
+          type: 'error',
+          message: error
+        })
+      }).finally(() => {
+        this.loading.unassign = false;
+      })
+    }
+  }
+}
+</script>
+
+<style>
+  .assign-dialog {
+    width: 65vw;
+    height: 90vh;
+    display: flex;
+    max-width: 1200px;
+    flex-direction: column;
+    margin-top: 5vh !important;
+  }
+  .assign-dialog .el-dialog__header {
+    padding: 20px 20px 0;
+    font-weight: bold;
+  }
+  .assign-dialog .el-dialog__body {
+    flex: 1;
+    padding: 20px;
+    display: flex;
+    overflow: hidden;
+    flex-direction: column;
+  }
+  .table-actions {
+    display: flex;
+    padding-top: 5px;
+    flex-wrap: wrap-reverse;
+    align-items: center;
+    justify-content: flex-end;
+  }
+  .table-view {
+    flex: 1;
+    overflow-y: auto;
+    padding-top: 10px;
+    margin-bottom: -10px;
+  }
+  @media screen and (max-width: 1200px) {
+    .assign-dialog {
+      width: 70vw;
+    }
+  }
+  @media screen and (max-width: 1000px) {
+    .assign-dialog {
+      width: 80vw;
+    }
+  }
+  @media screen and (max-width: 800px) {
+    .assign-dialog {
+      width: 90vw;
+    }
+  }
+  @media screen and (max-width: 700px) {
+    .assign-dialog {
+      width: 99vw;
+    }
+  }
+  @media screen and (max-width: 320px) {
+    .assign-dialog {
+      width: 100%;
+      min-width: 300px;
+    }
+  }
+</style>

+ 37 - 1
Strides-Admin/src/http/api/group.js

@@ -1,4 +1,4 @@
-import { get, post } from '../http'
+import { del, get, post, put, download } from '../http'
 
 const group = {
   getGroupPages(data) {
@@ -21,6 +21,42 @@ const group = {
   },
   getAllUserGroups() {
     return get('group/getUserGroups')
+  },
+  getGroupUserPages(data) {
+    return post('groups/user-page', data)
+  },
+  getGroupUserInfo(params) {
+    return get('groups/users', params)
+  },
+  addGroupUser(data) {
+    return post('groups/users', data)
+  },
+  updateGroupUser(data) {
+    return put('groups/users', data)
+  },
+  deleteGroupUser(data) {
+    return del('groups/users', data)
+  },
+  approveGroupUser(data) {
+    return get('groups/user-pass', data)
+  },
+  rejectGroupUser(data) {
+    return get('groups/user-reject', data)
+  },
+  downloadTemplate() {
+    return download('groups/user-template')
+  },
+  getAssignSitesPages(data) {
+    return post('group/getGroupAssignSitePages', data)
+  },
+  getAssignStatusOptions() {
+    return get('group/getAssignmentStatus')
+  },
+  assignSite2Group(data) {
+    return post('group/assignGroupSites', data)
+  },
+  unassignSite2Group(data) {
+    return post('group/unAssignGroupSites', data)
   }
 }
 

+ 39 - 0
Strides-Admin/src/http/api/rates.js

@@ -0,0 +1,39 @@
+import {get, post, put} from '../http'
+
+const rates = {
+  getRepeatOptions() {
+    return get("dynamicRate/getRepeat")
+  },
+  getRatePages(params) {
+    return post("dynamicRate/getDynamicRatePages", params)
+  },
+  addDynamicRate(params) {
+    return post("dynamicRate/addDynamicRate", params)
+  },
+  updateDynamicRate(params) {
+    return put("dynamicRate/updateDynamicRate", params)
+  },
+  deleteDynamicRate(params) {
+    return get("dynamicRate/delDynamicRateDetail", params)
+  },
+  deleteDynamicRateItem(params) {
+    return get("dynamicRate/delDynamicRateItem", params)
+  },
+  getDynamicRateInfo(params) {
+    return get("dynamicRate/getDynamicRateDetail", params)
+  },
+  getAssignStatusOptions() {
+    return get('dynamicRate/getAssignmentStatus')
+  },
+  getAssignSitesPages(data) {
+    return post('dynamicRate/getDynamicRateAssignSitePages', data)
+  },
+  assignRate2Site(data) {
+    return post('dynamicRate/assignDynamicRateSites', data)
+  },
+  unassignRate2Site(data) {
+    return post('dynamicRate/unAssignDynamicRateSites', data)
+  }
+}
+
+export default rates;

+ 11 - 6
Strides-Admin/src/layout/components/Navbar.vue

@@ -44,8 +44,11 @@
               <span class="email"><{{email}}></span>
             </div>
           </div>
-          <el-dropdown-item divided command="logout" style="padding: 5px 20px;">
-            <span>Log Out</span>
+          <el-dropdown-item
+            divided
+            command="logout"
+            style="padding: 5px 20px;">
+            <span>Log out</span>
           </el-dropdown-item>
         </el-dropdown-menu>
       </el-dropdown>
@@ -98,7 +101,7 @@
             <span>Email</span>
           </el-dropdown-item-->
           <el-dropdown-item divided command="logout">
-            <span>Log Out</span>
+            <span>Log out</span>
           </el-dropdown-item>
         </el-dropdown-menu>
       </el-dropdown>
@@ -144,7 +147,7 @@ export default {
       this.$store.dispatch('app/toggleSideBar')
     },
     logout() {
-      this.$confirm('Are you sure you want to logout ?', 'Logout', {
+      this.$confirm('Are you sure you want to log out?', 'Log out', {
         confirmButtonText: 'OK',
         cancelButtonText: 'Cancel',
         type: 'warning',
@@ -152,6 +155,7 @@ export default {
         const path = this.$router.currentRoute.fullPath
         //console.log('lougout+path', path);
         this.$store.dispatch("user/logout").then(res => {
+          this.$store.commit('permission/SET_ROUTES', []);
           this.$router.push({ path: "/login?redirect=" + path});
         })
       }).catch(err => {
@@ -247,7 +251,7 @@ export default {
 
     .right-menu-item {
       display: flex;
-      color: #555;
+      color: #333;
       font-size: 24px;
       padding: 0 30px;
       align-items: center;
@@ -259,12 +263,13 @@ export default {
       color: #888;
       font-size: 24px;
       padding: 8px 20px;
+      transition: all .4s;
       border-left: 1px solid #eee;
       .bold {
         font-weight: bold;
       }
       &:hover {
-        color: #555;
+        color: #333;
       }
     }
   }

+ 31 - 1
Strides-Admin/src/router/SiteRouter.js

@@ -23,7 +23,7 @@ export default {
     },
     {
       path: '/site-management/dynamic-rate-configuration',
-      component: () => import('@/views/site/SiteManagement'),
+      component: () => import('@/views/site/DynamicRates'),
       hidden: true,
       name: 'dynamic-rate-configuration',
       meta: {
@@ -68,6 +68,36 @@ export default {
         title: 'Manage Connectors',
         activeMenu: '/site-management/site-configuration'
       }
+    },
+    {
+      path: '/site-management/dynamic-rate-add',
+      component: () => import('@/views/site/RateDetail'),
+      hidden: true,
+      name: 'dynamic-rate-add',
+      meta: {
+        breadcrumb: true,
+        title: 'Create',
+        parent: {
+          title: 'Dynamic Rate',
+          path: "/site-management/dynamic-rate-configuration"
+        },
+        activeMenu: '/site-management/dynamic-rate-configuration'
+      }
+    },
+    {
+      path: '/site-management/dynamic-rate-update/:id',
+      component: () => import('@/views/site/RateDetail'),
+      hidden: true,
+      name: 'dynamic-rate-update',
+      meta: {
+        breadcrumb: true,
+        title: 'Edit',
+        parent: {
+          title: 'Dynamic Rate',
+          path: "/site-management/dynamic-rate-configuration"
+        },
+        activeMenu: '/site-management/dynamic-rate-configuration'
+      }
     }
   ]
 }

+ 34 - 4
Strides-Admin/src/router/UserRouter.js

@@ -21,7 +21,7 @@ export default {
     },
     {
       path: '/user-management/group',
-      component: () => import('@/views/driver/index'),
+      component: () => import('@/views/group/index'),
       name: 'DriverList',
       meta: {
         title: 'Group User',
@@ -40,9 +40,9 @@ export default {
       }
     },
     {
-      path: '/user-management/group/detail',
-      component: () => import('@/views/driver/DriverDetail'),
-      name: 'DriverDetail',
+      path: '/user-management/group/detail/:user',
+      component: () => import('@/views/group/detail'),
+      name: 'GroupUserDetail',
       meta: {
         title: 'Details',
         breadcrumb: true,
@@ -54,6 +54,36 @@ export default {
       },
       hidden: true
     },
+    {
+      path: '/user-management/group/edit/:id',
+      component: () => import('@/views/group/detail'),
+      name: 'EditGroupUser',
+      meta: {
+        title: 'Update',
+        breadcrumb: true,
+        activeMenu: '/user-management/group',
+        parent: {
+          title: 'Group User',
+          path: "/user-management/group"
+        }
+      },
+      hidden: true
+    },
+    {
+      path: '/user-management/group/add',
+      component: () => import('@/views/group/detail'),
+      name: 'AddGroupUser',
+      meta: {
+        title: 'Add',
+        breadcrumb: true,
+        activeMenu: '/user-management/group',
+        parent: {
+          title: 'Group User',
+          path: "/user-management/group"
+        }
+      },
+      hidden: true
+    },
     {
       path: '/user-management/add-user',
       component: () => import('@/views/user/UserDetail'),

+ 1 - 1
Strides-Admin/src/router/addition/RfidRouter.js

@@ -28,7 +28,7 @@ export default {
       name: 'rfid-management-add',
       meta: {
         title: 'Add User',
-        activeMenu: '/rfid-management'
+        activeMenu: '/rfid-card-management'
       }
     },
     {

+ 13 - 0
Strides-Admin/src/styles/btn.scss

@@ -100,4 +100,17 @@
 
 .el-button.el-button--content:not(.is-disabled) {
   color: #232323;
+}
+
+.el-button.el-button--accent {
+  color: $buttonText;
+  background: $--color-accent;
+  &.is-disabled {
+    color: $buttonText;
+    background-color: rgba($--color-accent, .5);
+  }
+  &:not(.is-disabled):hover {
+    color: $buttonText;
+    background-color: rgba($--color-accent, .7);
+  }
 }

+ 7 - 0
Strides-Admin/src/styles/index.scss

@@ -341,6 +341,7 @@ aside {
 
 .filter-view {
   display: flex;
+  margin: 0 -5px;
   flex-wrap: wrap;
   align-items: center;
 }
@@ -377,4 +378,10 @@ input:-webkit-autofill:active  {
 
 .el-table.no-border:before {
   height: 0 !important;
+}
+
+.el-checkbox-button.is-focus:not(.is-checked) {
+  .el-checkbox-button__inner {
+    border-color: #DCDFE6;
+  }
 }

+ 4 - 3
Strides-Admin/src/styles/variables.scss

@@ -8,6 +8,9 @@ $tiffany: #4ab7bd;
 $yellow: #fec171;
 $panGreen: #30b08f;
 
+$navigationBarHeight: 64px;
+$mainAppMinHeight: calc(100vh - #{$navigationBarHeight});
+
 // sidebar
 $menuText: #000;
 $menuActiveText: #fff;
@@ -22,10 +25,8 @@ $subMenuHover: #909DD8;
 $itemActiveBg: #001489;
 $subMenuActiveBg: #FFF;
 
-// size
 $sideBarWidth: 300px;
-$navigationBarHeight: 64px;
-$mainAppMinHeight: calc(100vh - #{$navigationBarHeight});
+$buttonText: #FFF;
 
 // the :export directive is the magic sauce for webpack
 // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass

+ 1 - 0
Strides-Admin/src/views/access/index.vue

@@ -167,6 +167,7 @@ export default {
           type: 'success',
           message: "Successfully deleted!"
         })
+        this.getTableData()
       }).catch(err => {
         this.$message({
           type: 'error',

+ 55 - 6
Strides-Admin/src/views/company/detail.vue

@@ -35,11 +35,22 @@
         <el-form-item
           prop="contactPerson"
           label="Contact Person:">
-          <el-input
-            v-model="form.contactPerson"
-            class="add-text"
-            placeholder=""
-            maxlength="80"/>
+          <div class="add-text flexc">
+            <el-select
+              style="min-width: 75px; max-width: 80px;"
+              v-model="form.callingCode">
+              <el-option
+                v-for="item in options.callingCode"
+                :key="item.callingCode"
+                :label="'+' + item.callingCode"
+                :value="item.callingCode"
+              />
+            </el-select>
+            <el-input
+              style="margin-left: 10px;"
+              v-model="form.contactPerson"
+              maxlength="80"/>
+          </div>
         </el-form-item>
         <el-form-item
           prop="contactNumber"
@@ -80,6 +91,14 @@
             </el-option>
           </el-select>
         </el-form-item>
+        <el-form-item
+          prop="discount"
+          label="Discount:">
+          <el-input
+            v-model="form.discount"
+            class="add-text"
+            maxlength="5"/>
+        </el-form-item>
       </div>
       <div class="content flexcr">
         <div class="buttons">
@@ -113,12 +132,14 @@
 import site from '@/http/api/site'
 import api from '@/http/api/group'
 import setting from '../../settings.js'
+import {getCountryList} from '../../utils/index.js'
 export default {
   name: "GroupDetail",
   data() {
     return {
       loading: false,
       form: {
+        discount: "",
         groupPk: "",
         groupName: "",
         groupType: "",
@@ -126,9 +147,11 @@ export default {
         contactNumber: "",
         loginId: "",
         password: "",
+        callingCode: setting.defaultCalling,
         countryCode: setting.defaultCountry
       },
       options: {
+        callingCode: [],
         countryOptions: [],
         groupType: []
       },
@@ -153,7 +176,27 @@ export default {
           message: "required",
           trigger: "blur",
           required: false,
+        }, {
+          pattern: /^\d{6,}$/,
+          trigger: "blur",
+          message: "Please enter the correct number",
         }],
+        discount: [{
+          trigger: "blur",
+          validator: (rule, value, callback, source, options) => {
+            let pattern = /^\d*(\.)?\d{1,2}$/
+            if (pattern.test(value)) {
+              const lang = Number(value);
+              if (lang < 0 || lang > 100) {
+                callback("Discount must to be between 0 and 100")
+              } else {
+                callback()
+              }
+            } else {
+              callback("Please enter the correct discount")
+            }
+          }
+        }]
       }
     }
   },
@@ -167,6 +210,9 @@ export default {
   },
   methods: {
     getCountryList() {
+      getCountryList(list => {
+        this.options.callingCode = list
+      })
       site.getCountryList().then(({ data }) => {
         this.options.countryOptions = data
       })
@@ -196,6 +242,9 @@ export default {
       }).then(res => {
         if (res.data) {
           this.form = Object.assign(this.form, res.data)
+          if (!this.form.callingCode) {
+            this.form.callingCode = setting.defaultCalling
+          }
         }
       }).catch(err => {
         this.$message({
@@ -256,7 +305,7 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-@import '../../styles/variables.scss';
+  @import '../../styles/variables.scss';
   .container {
     width: 100%;
     padding: 20px 60px;

+ 90 - 13
Strides-Admin/src/views/company/index.vue

@@ -17,7 +17,7 @@
               Search
             </el-button>
           </div>
-          <div class="filter-flex-button">
+          <div class="filter-flex-button" v-if="!$route.meta.onlyView">
             <el-button
               icon="el-icon-plus"
               type="primary"
@@ -28,42 +28,97 @@
         </div>
       </el-form>
     </div>
-    <el-table :data="table.list">
-      <el-table-column
+    <el-table
+      :data="table.list"
+      class="no-border">
+      <!-- <el-table-column
         align="center"
         label="Group ID"
         prop="groupPk"
-      ></el-table-column>
+      ></el-table-column> -->
       <el-table-column
         align="center"
         label="Name"
         prop="groupName"
-      ></el-table-column>
+        min-width="200"
+        fixed="left">
+        <template v-slot="{ row }">
+          <div
+            class="link-type"
+            v-if="!$route.meta.onlyView"
+            @click="onClickEditButton(row)">
+            {{row.groupName}}
+          </div>
+          <div v-else>{{row.groupName}}</div>
+        </template>
+      </el-table-column>
       <el-table-column
         align="center"
         label="Type"
         prop="groupType"
+        min-width="120"
       ></el-table-column>
       <el-table-column
         align="center"
         label="Approved"
-        prop="approvedCount"/>
+        prop="approvedCount"
+        min-width="120"/>
       <el-table-column
         align="center"
         label="Pending"
-        prop="pendingCount"/>
+        prop="pendingCount"
+        min-width="120"/>
       <!-- <el-table-column
         align="center"
         label="No. Of Drivers"
         prop="noOfDrivers"/> -->
+      <el-table-column
+        align="center"
+        label="Country"
+        prop="country"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Discount"
+        prop="discount"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Eligible Sites (Discount)"
+        prop="eligibleSiteCount"
+        min-width="120">
+        <template slot="header">
+          Eligible Sites<br/><span style="white-space: nowrap;">(Discount)</span>
+        </template>
+      </el-table-column>
       <el-table-column
         align="center"
         label="Action"
-        width="180">
+        min-width="100"
+        v-if="!$route.meta.onlyView">
         <template v-slot="{ row }">
-          <TableAction
+          <el-dropdown
+            class="action-dropdown"
+            @command="(v) => handleCommand(v, row)">
+            <i class="el-icon-more icon-action"></i>
+            <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item
+                command="assignSites">
+                Assign Sites (Discount)
+              </el-dropdown-item>
+              <el-dropdown-item
+                command="onClickEditButton">
+                Edit Info
+              </el-dropdown-item>
+              <el-dropdown-item
+                command="onClickDeleteButton">
+                Delete
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+          <!-- <TableAction
             @edit="onClickEditButton(row)"
-            @delete="onClickDeleteButton(row)"/>
+            @delete="onClickDeleteButton(row)"/> -->
         </template>
       </el-table-column>
     </el-table>
@@ -75,16 +130,22 @@
         :limit.sync="table.pageSize"
         @pagination="handlePageChange" />
     </div>
+    <DialogAssignment
+      :visible="assign.visible"
+      :title="'ASSIGN SITES (DISCOUNT: ' + assign.item.discount + ')'"
+      :group="assign.item"
+      @hide="assignSites"/>
   </div>
 </template>
 
 <script>
 import TableAction from '@/components/TableAction.vue'
 import Pagination from '@/components/Pagination'
+import DialogAssignment from '@/components/DialogAssignment'
 import api from '@/http/api/group'
 
 export default {
-  components: { Pagination, TableAction },
+  components: { Pagination, TableAction, DialogAssignment },
   data() {
     return {
       form: {
@@ -96,6 +157,10 @@ export default {
         pageNo: 1,
         pageSize: 10,
       },
+      assign: {
+        item: {},
+        visible: false
+      }
     }
   },
   methods: {
@@ -117,7 +182,6 @@ export default {
         this.table.total = total
       }
     },
-
     onClickSearch() {
       this.table.pageNo = 1
       this.getGroupPages()
@@ -136,6 +200,19 @@ export default {
         this.onDeleteCompany(row);
       })
     },
+    handleCommand(cb, item) {
+      this[cb](item)
+    },
+    assignSites(row) {
+      if (row) {
+        this.assign.item = row;
+        this.assign.visible = true;
+      } else {
+        this.assign.item = {};
+        this.assign.visible = false;
+        this.getGroupPages();
+      }
+    },
     onClickEditButton(row) {
       this.$router.push({
         path: "/partnership-management/group-management/edit/" + row.groupPk
@@ -152,7 +229,7 @@ export default {
           })
           this.getGroupPages()
         }
-      }).then(error => {
+      }).catch(error => {
         this.$message({
           type: 'error',
           message: error

+ 0 - 2
Strides-Admin/src/views/dashboard/components/Maps.vue

@@ -289,7 +289,6 @@ export default {
   }
 
   .card-group {
-    padding: 0 5px;
     display: flex;
     flex-wrap: wrap;
     flex-flow: wrap;
@@ -403,7 +402,6 @@ export default {
   }
 
   .google-map-container {
-    margin: 0 5px;
     padding: 0px;
     background-color: white;
   }

+ 0 - 1
Strides-Admin/src/views/dashboard/components/Site.vue

@@ -109,7 +109,6 @@
 
 <style scoped>
   .card-group {
-    padding: 0 5px;
     display: flex;
     flex-wrap: wrap;
     flex-flow: wrap;

+ 0 - 1
Strides-Admin/src/views/dashboard/components/Stations.vue

@@ -110,7 +110,6 @@
 
 <style scoped lang="scss">
   .card-group {
-    padding: 0 5px;
     display: flex;
     flex-wrap: wrap;
     flex-flow: wrap;

+ 4 - 3
Strides-Admin/src/views/dashboard/components/Summary.vue

@@ -275,7 +275,7 @@ export default {
   //主题色 $--color-primary
   //强调色 $--color-accent
   .summary-view {
-    margin: 5px 0px 0;
+    margin: 5px -5px 0;
   }
   .summary-view > div {
     margin: 5px;
@@ -354,10 +354,11 @@ export default {
   .charts-card {
     display: flex;
     overflow: hidden;
-    flex-direction: column;
-    margin: 15px 5px 0;
     padding: 10px 20px;
+    margin-top: 5px;
+    margin-bottom: 10px;
     border-radius: 6px;
+    flex-direction: column;
     background-color: white;
   }
   .chart-title {

+ 556 - 0
Strides-Admin/src/views/group/detail.vue

@@ -0,0 +1,556 @@
+<template>
+  <div
+    class="container"
+    v-loading="loading.page">
+    <el-form
+      class="content"
+      ref="form"
+      :model="form"
+      :rules="rules"
+      label-width="150px"
+      label-position="right">
+      <div class="section-title">Memberships</div>
+      <div class="flexcr">
+        <el-form-item
+          label="Display Name:"
+          prop="displayName"
+          class="flex1">
+          <el-input
+            class="add-text"
+            :readonly="isView"
+            v-model="form.displayName"/>
+        </el-form-item>
+        <el-form-item
+          label="Card No:"
+          prop="cardNo"
+          class="flex1">
+          <el-input
+            class="add-text"
+            :readonly="isView"
+            v-model="form.cardNo"/>
+        </el-form-item>
+      </div>
+      <div class="flexcr">
+        <el-form-item
+          label="Email Address:"
+          prop="emailAddress"
+          class="flex1">
+          <el-input
+            class="add-text"
+            :readonly="isView"
+            v-model="form.emailAddress"/>
+        </el-form-item>
+        <el-form-item
+          label="Group:"
+          prop="groupPk"
+          class="flex1">
+          <el-select
+            class="add-text"
+            :disabled="isView"
+            v-model="form.groupPk">
+            <el-option
+              v-for="(item, index) in options.groups"
+              :key="index"
+              :label="item.groupName"
+              :value="''+item.groupPk"/>
+          </el-select>
+        </el-form-item>
+      </div>
+      <div class="flexcr">
+        <el-form-item
+          label="Country:"
+          prop="countryCode"
+          class="flex1">
+          <el-select
+            class="add-text"
+            :disabled="isView"
+            v-model="form.countryCode">
+            <el-option
+              v-for="item in options.countryList"
+              :key="item.value"
+              :label="item.name"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          label="Phone Number:"
+          prop="phone"
+          class="flex1">
+          <div class="add-text flexc">
+            <el-select
+              style="min-width: 75px; max-width: 80px;"
+              :disabled="isView"
+              v-model="form.callingCode">
+              <el-option
+                v-for="item in options.callingCode"
+                :key="item.callingCode"
+                :label="'+' + item.callingCode"
+                :value="item.callingCode"
+              />
+            </el-select>
+            <el-input
+              style="margin-left: 10px;"
+              :readonly="isView"
+              v-model="form.phone"/>
+          </div>
+        </el-form-item>
+      </div>
+      <div class="flexcr">
+        <el-form-item
+          label="PDVL:"
+          class="form-photo"
+          prop="cardFront">
+          <template v-slot:label>
+            Card Photo:<br>(Front)&nbsp;
+          </template>
+          <div class="add-text">
+            <el-upload
+              v-if="isEdit || (!isEdit && !isView)"
+              class="photo-uploader"
+              accept=".jpg,.jpeg,.png,.gif,.JPG,.JPEG"
+              :action="action"
+              :headers="headers"
+              :show-file-list="false"
+              :on-success="onUploadSuccess"
+              :on-error="onUploadError"
+              :before-upload="beforeUpload">
+              <div
+                v-loading="loading.upload"
+                class="uploader-image">
+                <el-image
+                  class="uploader-image"
+                  v-if="form.cardFront"
+                  :src="getImageSrc(form.cardFront)"/>
+                <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+              </div>
+            </el-upload>
+            <div class="photo-uploader" v-else-if="form.cardFront">
+              <el-image
+                class="uploader-image"
+                :src="getImageSrc(form.cardFront)"
+                :preview-src-list="previewSrcList"/>
+            </div>
+          </div>
+        </el-form-item>
+      </div>
+      <p></p>
+    </el-form>
+    <div class="content flexcr">
+      <div class="buttons" v-if="isView">
+        <el-button
+          v-if="this.form.membershipStatus !== 'Reject'"
+          @click="onClickReject"
+          type="primary"
+          class="cancel-button"
+          :loading="loading.reject">
+          &nbsp;Reject&nbsp;
+        </el-button>
+        <el-button
+          v-if="this.form.membershipStatus !== 'Pass'"
+          @click="onClickApprove"
+          type="primary"
+          :loading="loading.approve">
+          Approve
+        </el-button>
+      </div>
+      <div class="buttons" v-else-if="isEdit">
+        <el-button
+          @click="onBack"
+          type="primary"
+          class="cancel-button">
+          Cancel
+        </el-button>
+        <el-button
+          @click="onClickSave"
+          type="primary"
+          :loading="loading.save">
+          &nbsp;Save&nbsp;
+        </el-button>
+      </div>
+      <div class="buttons" v-else>
+        <el-button
+          @click="onBack"
+          type="primary"
+          class="cancel-button">
+          &nbsp;Back&nbsp;
+        </el-button>
+        <el-button
+          @click="onClickSave"
+          type="primary"
+          :loading="loading.save">
+          &nbsp;Save&nbsp;
+        </el-button>
+      </div>
+      <div class="update-by" v-if="isEdit || isView">
+        <span
+          class="add-text"
+          :title='"CREATED BY " + form.createdBy + " ON " + form.createdOn'>
+          LAST UPDATED BY {{form.updatedBy}} TIMESTAMP: {{form.updatedOn}}
+        </span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import api from '@/http/api/group'
+import site from '@/http/api/site'
+import { baseURL } from '@/http/http'
+import { getToken } from '@/utils/auth'
+import settings from '../../settings.js'
+import {getCountryList} from '../../utils/index.js'
+export default {
+  data() {
+    return {
+      loading: {
+        page: true,
+        upload: false,
+        approve: false,
+        reject: false,
+        save: false
+      },
+      form: {
+        membershipId: "",
+        displayName: "",
+        cardNo: "",
+        countryCode: settings.defaultCountry,
+        callingCode: settings.defaultCalling,
+        emailAddress: "",
+        groupPk: "",
+        phone: "",
+        cardFront: "",
+        cardReverse: "",
+        membershipStatus: "",
+      },
+      rules: {
+        displayName: {
+          required: true,
+          trigger: "blur",
+          message: "Please input display name",
+        },
+        emailAddress: {
+          required: true,
+          trigger: "blur",
+          message: "Please input email address"
+        },
+        cardNo: {
+          required: true,
+          trigger: "blur",
+          message: "Please input card number"
+        },
+        groupPk: {
+          required: true,
+          trigger: "change",
+          message: "Please select a group"
+        },
+        countryCode: {
+          required: true,
+          trigger: "change",
+          message: "Please select a country"
+        },
+        phone: {
+          required: true,
+          trigger: "blur",
+          message: "Please input phone number"
+        },
+        cardFront: {
+          required: true,
+          trigger: "change",
+          message: "Please upload card photo"
+        }
+      },
+      options: {
+        groups: [],
+        callingCode: [],
+        countryList: []
+      },
+      isView: false,
+      isEdit: false
+    }
+  },
+  computed: {
+    action() {
+      return baseURL + process.env.VUE_APP_API_PREFIX + '/picture/upload'
+    },
+    headers() {
+      return {
+        accessToken: getToken(),
+        photoSubDir: "MEMBERSHIP",
+      }
+    },
+    previewSrcList() {
+      const imageSrc = []
+      if (this.form.cardFront) {
+        imageSrc.push(baseURL + this.form.cardFront)
+      }
+      return imageSrc
+    },
+    isFulfilled() {
+      return this.form.membershipStatus !== 'Pending'
+    },
+  },
+  created() {
+    console.log("参数", );
+    const params = this.$route.params
+    if (params.user) {
+      this.isView = true;
+      this.form.membershipId = params.user;
+    } else if (params.id) {
+      this.isEdit = true;
+      this.form.membershipId = params.id;
+    }
+    this.getCountryOptions()
+    this.getGroupOptions()
+    getCountryList(list => {
+      this.options.callingCode = list
+    })
+  },
+  methods: {
+    onBack() {
+      this.$nextTick(() => {
+        this.$router.replace({
+          path: "/user-management/group"
+        })
+      })
+    },
+    getImageSrc(path) {
+      return baseURL + path
+    },
+    getCountryOptions() {
+      site.getCountryList().then(res => {
+        if (res.data) {
+          this.options.countryList = res.data
+        }
+      }).catch(err => {
+        this.loading.page = false;
+        this.$message({
+          message: err,
+          type: 'error'
+        })
+      })
+    },
+    getGroupOptions() {
+      api.getAllUserGroups().then(res => {
+        if (res.data) {
+          this.options.groups = res.data
+        }
+        if (this.isEdit || this.isView) {
+          this.getGroupUserInfo();
+        } else {
+          setTimeout(() => {
+            this.loading.page = false;
+          }, 500)
+        }
+      }).catch(err => {
+        this.loading.page = false;
+        this.$message({
+          message: err,
+          type: 'error'
+        })
+      })
+    },
+    getGroupUserInfo() {
+      api.getGroupUserInfo({
+        membershipId: this.form.membershipId
+      }).then(res => {
+        this.loading.page = false;
+        if (res.data) {
+          this.form = res.data
+        }
+      }).catch(err => {
+        this.loading.page = false;
+        this.$message({
+          message: err,
+          type: 'error'
+        })
+      })
+    },
+    beforeUpload(file) {
+      const IMAGE_TYPE_ARRAY = ['jpg', 'png', 'jpeg', 'gif']
+      const fileExt = file.name.replace(/.+\./, '').toLowerCase()
+      if (IMAGE_TYPE_ARRAY.indexOf(fileExt) === -1) {
+        const msg = `Please select a file with ${IMAGE_TYPE_ARRAY.join(', ')}`
+        this.$message.warning(msg)
+        return false
+      }
+      if (file.size > 2097152) {
+        this.$message.warning('The file cannot exceed 2MiB')
+        return false
+      }
+      this.loading.upload = true;
+    },
+    onUploadSuccess(response) {
+      this.form.cardFront = response.data.picturePath;
+      this.loading.upload = false;
+    },
+    onUploadError(err) {
+      this.$message({
+        type: 'error',
+        message: "" + err
+      });
+      this.loading.upload = false;
+    },
+    onClickReject() {
+      this.loading.reject = true;
+      api.rejectGroupUser({
+        membershipId: this.form.membershipId
+      }).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Success"
+        });
+        this.onBack();
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        });
+      }).finally(() => {
+        this.loading.reject = false;
+      });
+    },
+    onClickApprove() {
+      this.loading.approve = true;
+      api.approveGroupUser({
+        membershipId: this.form.membershipId
+      }).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Success"
+        });
+        this.onBack();
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        });
+      }).finally(() => {
+        this.loading.approve = false;
+      });
+    },
+    onClickSave() {
+      this.$refs.form.validate(result => {
+        if (result) {
+          this.loading.save = true;
+          this.isEdit ? this.updateGroupuser() : this.addGroupUser();
+        }
+      });
+    },
+    addGroupUser() {
+      api.addGroupUser(this.form).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Successfully added"
+        });
+        this.onBack();
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        });
+      }).finally(() => {
+        this.loading.save = false;
+      });
+    },
+    updateGroupuser() {
+      api.updateGroupUser(this.form).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Successfully updated"
+        });
+        this.onBack();
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        });
+      }).finally(() => {
+        this.loading.save = false;
+      });
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  @import '../../styles/variables.scss';
+  .container {
+    width: 100%;
+    padding: 20px 60px;
+    min-height: $mainAppMinHeight;
+    background-color: #F0F5FC;
+  }
+  .content {
+    margin: 0 8px 16px;
+    padding: 15px 80px;
+    border-radius: 6px;
+    background-color: white;
+  }
+  
+  .section-title {
+    color: #333;
+    margin-top: 20px;
+    margin-bottom: 30px;
+    font-size: 15px;
+    user-select: none;
+    line-height: 24px;
+    font-weight: bold;
+    font-family: sans-serif;
+    text-transform: uppercase;
+  }
+  .add-text {
+    width: 100%;
+    min-width: 100px;
+    max-width: 300px;
+  }
+  .add-text ::v-deep .el-textarea__inner {
+    font-family: sans-serif;
+  }
+  .form-photo {
+    flex: 1;
+    ::v-deep .el-form-item__label {
+      padding: 12px;
+      line-height: 16px;
+    }
+    .photo-uploader {
+      margin-right: 10px;
+      .uploader-image {
+        width: 180px;
+        height: 120px;
+        text-align: left;
+      }
+      ::v-deep img {
+        object-fit: cover;
+      }
+      .avatar-uploader-icon {
+        border: 1px dashed #d9d9d9;
+        border-radius: 6px;
+        cursor: pointer;
+        font-size: 28px;
+        color: #8c939d;
+        width: 120px;
+        height: 120px;
+        line-height: 120px;
+        text-align: center;
+      }
+    }
+  }
+  .hr {
+    height: 2px;
+    margin: 10px -40px;
+    background-color: #F0F5FC;
+  }
+  .buttons {
+    padding-top: 15px;
+    padding-bottom: 15px;
+  }
+  @media screen and (max-width: 500px) {
+    .container {
+      padding: 0px;
+    }
+    .content {
+      padding: 15px 30px;
+    }
+  }
+</style>

+ 303 - 0
Strides-Admin/src/views/group/index.vue

@@ -0,0 +1,303 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <div class="filter-view">
+        <el-input
+          class="filter-input"
+          v-model="filters.pageVo.criteria"
+          placeholder="Search by Email, Phone or Group Name"
+          clearable/>
+        <div>
+          <el-button
+            @click="onClickSearch"
+            icon="el-icon-search"
+            type="primary">
+            Search
+          </el-button>
+        </div>
+        <div class="filter-flex-button"></div>
+        <my-upload
+          accept=".xls,.xlsx,.csv"
+          :limit="1"
+          :is-blob="true"
+          :action="action"
+          :headers="headers"
+          :file-list="fileList"
+          :show-file-list="false"
+          :before-upload="onImportStart"
+          :on-success="onImportExcel"
+          :on-error="onImportExcelErr"
+          v-if="!$route.meta.onlyView">
+          <el-button
+            icon="el-icon-upload2"
+            type="primary"
+            :loading="loading.upload">
+            Import Excel
+          </el-button>
+        </my-upload>
+        <div v-if="!$route.meta.onlyView">
+          <el-button
+            icon="el-icon-download"
+            type="primary"
+            :loading="loading.download"
+            @click="onDownloadTmp">
+            Download Template
+          </el-button>
+        </div>
+        <div v-if="!$route.meta.onlyView">
+          <el-button
+            icon="el-icon-plus"
+            type="primary"
+            @click="onAddUser">
+            Add Group User
+          </el-button>
+        </div>
+      </div>
+    </div>
+    <el-table
+      :data="table.list"
+      v-loading="loading.table">
+      <el-table-column
+        align="center"
+        label="Name"
+        prop="userPk">
+        <template slot-scope="{row}" >
+          <!-- <span
+            class="link-type"
+            @click="onViewUser(row)">
+            {{ row.membershipId }}
+          </span> -->
+          <span
+            class="link-type"
+            @click="onViewUser(row)"
+            v-if="!$route.meta.onlyView">
+            {{ row.nickName }}
+          </span>
+          <span v-else>{{ row.nickName }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        width="200"
+        align="center"
+        label="User Type"
+        prop="nickName">
+        <template slot-scope="{row}">
+          <span>Membership</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="Email"
+        prop="email"/>
+      <el-table-column
+        width="150"
+        align="center"
+        label="Phone"
+        prop="phone"/>
+      <el-table-column
+        align="center"
+        label="Group"
+        prop="groupName"/>
+      <el-table-column
+        align="center"
+        label="Status"
+        prop="membershipStatus"
+        width="150"/>
+      <el-table-column
+        align="center"
+        label="Action"
+        width="210"
+        v-if="!$route.meta.onlyView">
+        <template slot-scope="{ row }">
+          <TableAction
+            :showEdit="row.membershipStatus == 'Pending'"
+            @edit="onEditUser(row)"
+            @delete="onDeleteUser(row)"/>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="right">
+      <pagination
+        v-show="table.total > 0"
+        :total="table.total"
+        :page.sync="filters.pageNo"
+        :limit.sync="filters.pageSize"
+        @pagination="getTableData" />
+    </div>
+  </div>
+</template>
+
+<script>
+import Pagination from '@/components/Pagination'
+import TableAction from '@/components/TableAction'
+import MyUpload from '@/components/MyUpload'
+import api from '@/http/api/group'
+import { baseURL } from '@/http/http'
+import { getToken } from '@/utils/auth'
+export default {
+  data() {
+    return {
+      filters: {
+        pageNo: 1,
+        pageSize: 10,
+        pageVo: {
+          criteria: ""
+        }
+      },
+      table: {
+        list: [],
+        total: 0
+      },
+      loading: {
+        table: false,
+        upload: false,
+        download: false
+      },
+      fileList: []
+    }
+  },
+  computed: {
+    action() {
+      return baseURL + process.env.VUE_APP_API_PREFIX + '/groups/user-batch-create'
+    },
+    headers() {
+      return {
+        accessToken: getToken()
+      }
+    }
+  },
+  components: {
+    Pagination,
+    TableAction,
+    MyUpload
+  },
+  created() {
+    this.onClickSearch()
+  },
+  methods: {
+    onClickSearch() {
+      this.filters.pageNo = 1
+      this.getTableData()
+    },
+    getTableData() {
+      this.loading.table = true;
+      api.getGroupUserPages(this.filters).then(res => {
+        if (res.data && res.total) {
+          this.table.total = res.total;
+          this.table.list = res.data;
+        }
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        })
+        this.table.total = 0;
+        this.table.list = [];
+      }).finally(() => {
+        this.loading.table = false;
+      })
+    },
+    onAddUser() {
+      this.$router.push({ path: '/user-management/group/add' })
+    },
+    onViewUser(row) {
+      this.$router.push({
+        path: "/user-management/group/detail/" + row.membershipId
+      })
+    },
+    onEditUser(row) {
+      this.$router.push({
+        path: "/user-management/group/edit/" + row.membershipId
+      })
+    },
+    onDeleteUser(row) {
+      this.$confirm('Are you sure you want to delete this group user?', 'Delete', {
+        confirmButtonText: 'OK',
+        cancelButtonText: 'Cancel',
+        type: 'warning'
+      }).then(res => {
+        this.deleteUser(row.membershipId);
+      })
+    },
+    deleteUser(id) {
+      this.loading.table = true;
+      api.deleteGroupUser({
+        membershipId: id
+      }).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Delete success."
+        })
+        this.getTableData()
+      }).catch(err => {
+        this.loading.table = false;
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      })
+    },
+    onImportStart() {
+      this.loading.upload = true;
+    },
+    onImportExcel(res, file, fileList) {
+      fileList = [];
+      this.fileList = [];
+      if (res.success == undefined) {
+        this.downloadExcel(res, "batch_create_result.xls");
+        this.loading.upload = false;
+      } else {
+        this.onImportExcelErr(res.msg, file, fileList);
+      }
+    },
+    onImportExcelErr(err, file, fileList) {
+      this.$message({
+        type: 'error',
+        message: err
+      })
+      this.loading.upload = false;
+    },
+    onDownloadTmp() {
+      this.loading.download = true;
+      api.downloadTemplate().then(res => {
+        this.downloadExcel(res, "group-user-template.xlsx")
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      }).finally(() => {
+        this.loading.download = false;
+      })
+    },
+    downloadExcel(res, fileName) {
+      const blob = new Blob([res], {
+        type: 'application/vnd.ms-excel;charset=utf-8'
+      })
+      // let href = window.URL.createObjectURL(blob)
+      if ('download' in document.createElement('a')) {
+        // 非IE下载
+        const elink = document.createElement('a')
+        elink.download = fileName
+        elink.style.display = 'none'
+        elink.href = URL.createObjectURL(blob)
+        document.body.appendChild(elink)
+        elink.click()
+        URL.revokeObjectURL(elink.href) // 释放URL 对象
+        document.body.removeChild(elink)
+      } else {
+        // IE10+下载
+        navigator.msSaveBlob(blob, fileName)
+      }
+    },
+  }
+}
+</script>
+
+
+<style lang="scss" scoped>
+  .filter-input {
+    min-width: 100px;
+    max-width: 300px;
+  }
+</style>

+ 1 - 1
Strides-Admin/src/views/posDevice/index.vue

@@ -19,7 +19,7 @@
         align="center"
         class-name="fixed-width">
         <template slot-scope="{row}">
-          <a class="link-detail" href="javascript:void(0);" @click="viewDevice(row)">{{ row.deviceSerialNo }}</a>
+          <a class="link-type" href="javascript:void(0);" @click="viewDevice(row)">{{ row.deviceSerialNo }}</a>
         </template>
       </el-table-column>
       <el-table-column

+ 224 - 0
Strides-Admin/src/views/site/DynamicRates.vue

@@ -0,0 +1,224 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <div class="filter-view">
+        <el-input
+          class="filter-input"
+          v-model="filters.pageVo.criteria"
+          placeholder="Search by Rate Name or Service Provider"
+          clearable/>
+        <div>
+          <el-button
+            @click="onClickSearch"
+            icon="el-icon-search"
+            type="primary">
+            Search
+          </el-button>
+        </div>
+        <div
+          class="filter-flex-button"
+          v-if="!$route.meta.onlyView">
+          <el-button
+            icon="el-icon-plus"
+            type="primary"
+            @click="onClickAdd">
+            Create Rate
+          </el-button>
+        </div>
+      </div>
+    </div>
+    <el-table
+      v-loading="loading"
+      :data="table.list">
+      <el-table-column
+        align="center"
+        label="Rate ID"
+        prop="userPk"
+        min-width="120">
+        <template slot-scope="{row}" >
+          <span
+            class="link-type"
+            @click="onClickEdit(row)"
+            v-if="!$route.meta.onlyView">
+            {{ row.dynamicRateId }}
+          </span>
+          <span v-else>{{ row.dynamicRateId }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="Rate Name"
+        prop="rateName"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="No. of Sites Configured"
+        prop="assignedSiteCount"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Status"
+        prop="dataStatus"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Update Time"
+        prop="updateTime"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Action"
+        min-width="100"
+        v-if="!$route.meta.onlyView">
+        <template v-slot="{ row }">
+          <el-dropdown
+            class="action-dropdown"
+            @command="(v) => handleCommand(v, row)">
+            <i class="el-icon-more icon-action"></i>
+            <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item
+                command="assignRates">
+                Assign Sites
+              </el-dropdown-item>
+              <el-dropdown-item
+                command="onClickEdit">
+                Edit
+              </el-dropdown-item>
+              <el-dropdown-item
+                command="onClickDelete">
+                Delete
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="right">
+      <pagination
+        v-show="table.total > 0"
+        :total="table.total"
+        :page.sync="filters.pageNo"
+        :limit.sync="filters.pageSize"
+        @pagination="getTableData" />
+    </div>
+    <DialogAssignment
+      :visible="assign.visible"
+      :title="'ASSIGN SITES (RATE NAME: ' + assign.item.rateName + ')'"
+      :rate="assign.item"
+      @hide="assignRates"/>
+  </div>
+</template>
+
+<script>
+import TableAction from '@/components/TableAction.vue'
+import Pagination from '@/components/Pagination'
+import DialogAssignment from '@/components/DialogAssignment'
+import api from '@/http/api/rates'
+export default {
+  components: { Pagination, TableAction, DialogAssignment },
+  data() {
+    return {
+      loading: false,
+      filters: {
+        pageNo: 1,
+        pageSize: 10,
+        pageVo: {
+          criteria: ""
+        }
+      },
+      table: {
+        list: [],
+        total: 0
+      },
+      assign: {
+        item: {},
+        visible: false
+      }
+    }
+  },
+  created() {
+    this.onClickSearch()
+  },
+  methods: {
+    onClickSearch() {
+      this.filters.pageNo = 1
+      this.getTableData()
+    },
+    getTableData() {
+      this.loading = true;
+      api.getRatePages(this.filters).then(res => {
+        if (res.data && res.total) {
+          this.table.total = res.total;
+          this.table.list = res.data;
+        }
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        })
+        this.table.total = 0;
+        this.table.list = [];
+      }).finally(() => {
+        this.loading = false;
+      })
+    },
+    handleCommand(cb, item) {
+      this[cb](item)
+    },
+    onClickAdd() {
+      this.$router.push({
+        path: "/site-management/dynamic-rate-add"
+      })
+    },
+    onClickEdit(row) {
+      this.$router.push({
+        path: "/site-management/dynamic-rate-update/" + row.dynamicRateId
+      })
+    },
+    onClickDelete(row) {
+      this.$confirm('Are you sure you want to delete this rate?', 'Delete', {
+        confirmButtonText: 'OK',
+        cancelButtonText: 'Cancel',
+        type: 'warning'
+      }).then(res => {
+        this.deleteDynamicRate(row.dynamicRateId);
+      })
+    },
+    assignRates(row) {
+      if (row) {
+        this.assign.item = row;
+        this.assign.visible = true;
+      } else {
+        this.assign.item = {};
+        this.assign.visible = false;
+        this.getTableData();
+      }
+    },
+    deleteDynamicRate(id) {
+      this.loading = true;
+      api.deleteDynamicRate({
+        dynamicRateId: id
+      }).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Delete success."
+        })
+        this.getTableData()
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        })
+        this.loading = false;
+      })
+    }
+  }
+}
+</script>
+
+<style>
+  .filter-input {
+    min-width: 100px;
+    max-width: 300px;
+  }
+</style>

+ 526 - 0
Strides-Admin/src/views/site/RateDetail.vue

@@ -0,0 +1,526 @@
+<template>
+  <div class="container">
+    <el-form
+      ref="form"
+      :model="form"
+      :rules="rules"
+      label-width="150px"
+      label-position="top">
+      <div class="content">
+        <div class="section-title">Rate Config</div>
+        <div class="flexcr">
+          <el-form-item
+            label="Name:"
+            prop="rateName"
+            class="add-input">
+            <el-input
+              v-model="form.rateName"/>
+          </el-form-item>
+          <el-form-item
+            label="Country:"
+            prop="countryCode"
+            class="add-input">
+            <el-select
+              v-model="form.countryCode">
+              <el-option
+                v-for="item in options.country"
+                :key="item.name"
+                :label="item.name"
+                :value="item.value" />
+            </el-select>
+          </el-form-item>
+          <el-form-item
+            label="Rate Type:"
+            prop="rateType"
+            class="add-input">
+            <el-input
+              v-model="form.rateType"/>
+          </el-form-item>
+        </div>
+        <div class="flexcr">
+          <label class="el-form-item__label">Repeat:</label>
+          <div class="repeat-view">
+            (&nbsp;
+            <div
+              class="link-type"
+              v-for="(item, index) in options.shortcut"
+              :key="index"
+              @click="handleShortcut(item)">
+              <span>{{item.name}}</span>
+            </div>
+            &nbsp;)
+            <el-tooltip
+              effect="dark"
+              content="This is an items for quickly selecting the repeats"
+              placement="right">
+              <i class="el-icon-question icon-help"></i>
+            </el-tooltip>
+          </div>
+        </div>
+        <div style="margin-bottom: 10px;">
+          <el-checkbox-group
+          v-model="form.repeats">
+          <el-checkbox-button
+            v-for="(item, index) in options.repeat"
+            :label="item.value"
+            :key="index">
+            {{item.name}}
+          </el-checkbox-button>
+        </el-checkbox-group>
+        </div>
+        <div class="flexcr">
+          <el-form-item
+            label="All Day:"
+            class="add-input">
+            <el-switch
+              v-model="form.allDay"
+              @change="changeAllday"/>
+          </el-form-item>
+          <template v-if="form.allDay">
+            <el-form-item
+              label="Start Time:"
+              class="add-input">
+              <el-input disabled/>
+            </el-form-item>
+            <el-form-item
+              label="End Time:"
+              class="add-input">
+              <el-input disabled/>
+            </el-form-item>
+          </template>
+          <template v-else>
+            <el-form-item
+              label="Start Time:"
+              class="add-input"
+              prop="startTime">
+              <el-time-picker
+                v-model="form.startTime"
+                format="HH:mm"
+                value-format="HH:mm"
+                clearable/>
+            </el-form-item>
+            <el-form-item
+              label="End Time Time:"
+              class="add-input"
+              prop="endTime">
+              <el-time-picker
+                v-model="form.endTime"
+                format="HH:mm"
+                value-format="HH:mm"
+                clearable/>
+            </el-form-item>
+          </template>
+        </div>
+      </div>
+      <div class="content">
+        <div class="section-title flexcr">
+          CHARGE SITE RATE
+          <div class="section-sub-title">(Currency Used: {{currencyData[form.countryCode]}})</div>
+        </div>
+        <charge-rate
+          v-model="ratesForm.chargeRates"/>
+      </div>
+      <div class="content" v-if="false">
+        <div class="section-title flexcr">
+          SPECIAL CHARGE RATE
+          <div class="section-sub-title">(Currency Used: {{currencyData[form.countryCode]}})</div>
+        </div>
+        <charge-rate
+          isSpecial
+          v-model="ratesForm.specialChargeRates"/>
+      </div>
+      <div class="content flexcr">
+        <div class="buttons">
+          <el-button
+            @click="onBack"
+            type="primary"
+            class="cancel-button">
+            Cancel
+          </el-button>
+          <el-button
+            @click="onClickSave"
+            type="primary"
+            :loading="loadingSave">
+            &nbsp;Save&nbsp;
+          </el-button>
+        </div>
+        <div class="update-by" v-if="isEdit">
+          <span
+            class="add-text"
+            :title='"CREATED BY " + form.createdBy + " ON " + form.createdOn'>
+            LAST UPDATED BY {{form.updatedBy}} TIMESTAMP: {{form.updatedOn}}
+          </span>
+        </div>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import site from '../../http/api/site'
+import api from '../../http/api/rates'
+import settings from '../../settings.js'
+import ChargeRate from './components/ChargeRate'
+export default {
+  data() {
+    return {
+      loading: false,
+      loadingSave: false,
+      isEdit: false,
+      form: {
+        dynamicRateId: "",
+        rateName: "",
+        countryCode: settings.defaultCountry,
+        repeats: [],
+        allDay: false,
+        startTime: "",
+        endTime: "",
+        rates: [],
+        specialRates: []
+      },
+      options: {
+        country: [],
+        repeat: [],
+        shortcut: [{
+          name: "Daily",
+          value: [],
+          all: true
+        },{
+          name: "Weekday",
+          value: ["Mon","Tue","Wed","Thu","Fri"]
+        },{
+          name: "Weekend",
+          value: ["Sat","Sun"]
+        }, {
+          name: "None",
+          value: []
+        }]
+      },
+      currencyData: {
+        SG: "S$"
+      },
+      ratesForm: {
+        chargeRates: [{
+          rate: '',
+          rateType: '',
+          chargeTypePk: '',
+          dynamicRateItemId: ''
+        }],
+        specialChargeRates: [{
+          rate: '',
+          rateType: '',
+          chargeTypePk: '',
+          groupPk: ''
+        }]
+      },
+      rules: {
+        rateName: {
+          required: true,
+          trigger: "blur",
+          message: "Please input rate name"
+        },
+        startTime: {
+          required: true,
+          trigger: "change",
+          message: "Please select start time"
+        },
+        endTime: {
+          required: true,
+          trigger: "change",
+          message: "Please select end time"
+        }
+      },
+      
+    }
+  },
+  components: {ChargeRate},
+  created() {
+    this.loading = true;
+    this.getCountryOptions();
+    this.getRepeatOptions();
+    if (this.$route.params.id) {
+      this.isEdit = true;
+      this.getRateDetail();
+    }
+  },
+  methods: {
+    onBack() {
+      this.$nextTick(() => {
+        this.$router.replace({
+          path: "/site-management/dynamic-rate-configuration"
+        })
+      })
+    },
+    getCountryOptions() {
+      site.getCountryList().then(res => {
+        if (res.data) {
+          this.options.country = res.data
+          const sign = {}
+          res.data.forEach(item => {
+            sign[item.value] = item.currencySymbol
+          })
+          this.currencyData = sign;
+        }
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        })
+        this.loading = false;
+      })
+    },
+    getRepeatOptions() {
+      api.getRepeatOptions().then(res => {
+        if (res.data) {
+          this.options.repeat = res.data
+        }
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      }).finally(() => {
+        this.loading = false;
+      })
+    },
+    getRateDetail() {
+      this.loading = true;
+      api.getDynamicRateInfo({
+        dynamicRateId: this.$route.params.id
+      }).then(res => {
+        if (res.data) {
+          this.form = res.data;
+          if (res.data.rates && res.data.rates.length) {
+            this.ratesForm.chargeRates = res.data.rates;
+          }
+        }
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      }).finally(() => {
+        this.loading = false;
+      })
+    },
+    handleShortcut(shortcut) {
+      const select = []
+      if (shortcut.all) {
+        this.options.repeat.forEach(item => {
+          select.push(item.value)
+        })
+      } else {
+        select.push(...shortcut.value)
+      }
+      this.form.repeats = select;
+    },
+    changeAllday(all) {
+      if (all) {
+        this.form.startTime = "";
+        this.form.endTime = "";
+        this.$refs.form.clearValidate()
+      }
+    },
+    onClickSave() {
+      this.$refs.form.validate(result => {
+        if (result) {
+          if (this.form.repeats.length == 0) {
+            this.$message({
+              message: "Please select at least one repeat day",
+              type: 'error',
+              duration: 3000,
+            })
+            return;
+          }
+          const rates = [];
+          this.ratesForm.chargeRates.forEach(item => {
+            if (item.rate) {
+              rates.push(item);
+            }
+          })
+          this.ratesForm.specialChargeRates.forEach(item => {
+            if (item.rate) {
+              rates.push(item);
+            }
+          })
+          if (rates.length == 0) {
+            this.$message({
+              message: "Please add at least one site rate",
+              type: 'error',
+              duration: 3000,
+            })
+            return;
+          }
+          this.form.rates = rates;
+          this.loadingSave = true;
+          this.isEdit ? this.updateRateData() : this.addRateData();
+        }
+      });
+    },
+    addRateData() {
+      api.addDynamicRate(this.form).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Successfully added"
+        });
+        this.onBack();
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        });
+      }).finally(() => {
+        this.loadingSave = false;
+      });
+    },
+    updateRateData() {
+      api.updateDynamicRate(this.form).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Successfully updated"
+        });
+        this.onBack();
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        });
+      }).finally(() => {
+        this.loadingSave = false;
+      });
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  @import '../../styles/variables.scss';
+  .container {
+    width: 100%;
+    padding: 20px 60px;
+    min-height: $mainAppMinHeight;
+    background-color: #F0F5FC;
+  }
+  .content {
+    margin: 0 8px 16px;
+    padding: 15px 80px;
+    border-radius: 6px;
+    background-color: white;
+  }
+  
+  .section-title {
+    color: #333;
+    margin-top: 20px;
+    margin-bottom: 30px;
+    font-size: 15px;
+    user-select: none;
+    line-height: 24px;
+    font-weight: bold;
+    font-family: sans-serif;
+    text-transform: uppercase;
+  }
+  
+  .section-sub-title {
+    font-size: 14px;
+    padding-left: 5px;
+    font-weight: normal;
+  }
+  
+  .add-text {
+    width: 100%;
+    min-width: 100px;
+    max-width: 300px;
+  }
+  .add-text ::v-deep .el-textarea__inner {
+    font-family: sans-serif;
+  }
+  .add-input {
+    width: 100%;
+    min-width: 100px;
+    max-width: 250px;
+    margin-right: 10px;
+    ::v-deep .el-input,
+    ::v-deep .el-select {
+      width: 100%;
+    }
+  }
+  
+  .icon-help {
+    color: #999;
+    font-size: 15px;
+    cursor: pointer;
+  }
+  
+  .form-photo {
+    flex: 1;
+    ::v-deep .el-form-item__label {
+      padding: 12px;
+      line-height: 16px;
+    }
+    .photo-uploader {
+      margin-right: 10px;
+      .uploader-image {
+        width: 180px;
+        height: 120px;
+        text-align: left;
+      }
+      ::v-deep img {
+        object-fit: cover;
+      }
+      .avatar-uploader-icon {
+        border: 1px dashed #d9d9d9;
+        border-radius: 6px;
+        cursor: pointer;
+        font-size: 28px;
+        color: #8c939d;
+        width: 120px;
+        height: 120px;
+        line-height: 120px;
+        text-align: center;
+      }
+    }
+  }
+  .repeat-view {
+    display: flex;
+    font-size: 14px;
+    font-weight: bold;
+    align-items: center;
+    padding: 0 10px 10px;
+    .link-type + .link-type {
+      margin-left: 5px;
+      &::before {
+        color: #333;
+        content: "|";
+        font-size: 15px;
+        font-weight: normal;
+        padding-right: 5px;
+      }
+    }
+  }
+  .hr {
+    height: 2px;
+    margin: 10px -40px;
+    background-color: #F0F5FC;
+  }
+  .buttons {
+    padding-top: 15px;
+    padding-bottom: 15px;
+  }
+  @media screen and (max-width: 1200px) {
+    .add-input {
+      min-width: 80px;
+      max-width: 200px;
+    }
+  }
+  @media screen and (max-width: 500px) {
+    .container {
+      padding: 0px;
+    }
+    .content {
+      padding: 15px 30px;
+    }
+    .add-input {
+      max-width: unset;
+    }
+  }
+</style>

+ 32 - 2
Strides-Admin/src/views/site/components/ChargeRate.vue

@@ -75,6 +75,7 @@
 import site from '@/http/api/site'
 import driver from '@/http/api/driver'
 import group from '@/http/api/group'
+import rates from '@/http/api/rates'
 export default {
   name: "ChargeRate",
   props: {
@@ -204,13 +205,17 @@ export default {
       //this.onChange();
     },
     handleClickSubButton(item, index) {
-      if (item.ratePk) {
+      if (item.ratePk || item.dynamicRateItemId) {
         this.$confirm('Are you sure you want to delete this charge rate ?', 'Delete', {
           confirmButtonText: 'Ok',
           cancelButtonText: 'Cancel',
           type: 'warning',
         }).then(() => {
-          this.deleteChargeRate(item, index);
+          if (item.ratePk) {
+            this.deleteChargeRate(item, index);
+          } else if (item.dynamicRateItemId) {
+            this.deleteDynamicRate(item, index);
+          }
         })
       } else {
         this.chargeRates.splice(index, 1);
@@ -232,6 +237,20 @@ export default {
           type: 'error',
         })
       })
+    },
+    deleteDynamicRate(item, index) {
+      rates.deleteDynamicRateItem({ dynamicRateItemId: item.dynamicRateItemId }).then(() => {
+        this.chargeRates.splice(index, 1);
+        if (this.chargeRates.length == 0) {
+          this.handleClickAddButton()
+        }
+      }).catch((error) => {
+        this.$notify({
+          title: 'Delete charge rate failed',
+          message: error,
+          type: 'error',
+        })
+      })
     }
   }
 }
@@ -243,6 +262,17 @@ export default {
   flex-wrap: wrap;
   align-items: center;
 }
+.rate-list-view >>> .el-form-item {
+  display: flex;
+  align-items: center;
+}
+.rate-list-view >>> .el-form-item__label {
+  font-size: 14px;
+  color: #606266;
+  text-align: left;
+  line-height: 40px;
+  padding: 0 12px 0 0;
+}
 .list-item-icon {
   width: 30px;
   height: 30px;

+ 5 - 4
Strides-Admin/src/views/site/detail.vue

@@ -211,7 +211,7 @@
       </div>
       
       <div class="view-content" id="idChargeRates">
-        <div class="section-title flexcr">
+        <!--div class="section-title flexcr">
           CHARGE SITE RATE
           <div class="section-sub-title">(Currency Used: {{currencyData[siteForm.address.countryCode]}})</div>
         </div>
@@ -225,7 +225,7 @@
         <charge-rate
           isSpecial
           v-model="ratesForm.specialChargeRates"/>
-        <div class="sparator"></div>
+        <div class="sparator"></div>-->
         <div class="section-title">Reservations</div>
         <reservation
           :enabled.sync="siteForm.enableReservation"
@@ -580,7 +580,7 @@ export default {
       setTimeout(() => {
         this.pageLoading = false;
       }, 500);
-      if (this.siteForm.chargeRates) {
+      if (this.siteForm.chargeRates && this.siteForm.chargeRates.length) {
         const rate = [], srate = []
         this.siteForm.chargeRates.forEach(item => {
           if (item.groupPk) {
@@ -659,6 +659,7 @@ export default {
       this.$refs['addForm'].validate(result => {
         if (result) {
           const rates = [];
+          /* 启用动态费率配置
           this.ratesForm.chargeRates.forEach(item => {
             if (item.rate) {
               rates.push(item);
@@ -677,7 +678,7 @@ export default {
             })
             this.scrollToView("idChargeRates", true);
             return;
-          }
+          }*/
           if (!this.siteForm.enableReservation) {
             this.siteForm.timeLimit = "";
           }

+ 38 - 62
Strides-Admin/src/views/user/UserManagement.vue

@@ -23,7 +23,7 @@
                 Search
             </el-button>
           </div>
-          <div class="filter-flex-button">
+          <div class="filter-flex-button" v-if="!$route.meta.onlyView">
             <el-button
               type="primary"
               icon="el-icon-plus"
@@ -34,90 +34,69 @@
         </div>
       </el-form>
     </div>
-    
-
     <el-table
       :key="tableKey"
       v-loading="listLoading"
       :data="list"
-      fit
       style="width: 100%;">
-    >
       <el-table-column
-        width="100"
-        label="User ID"
+        label="Name"
         prop="userId"
-        align="center"
-        class-name="fixed-width">
-          <template slot-scope="{row}">
-            <span>{{ row.userId }}</span>
-          </template>
+        align="center">
+        <template slot-scope="{row}">
+          <div
+            class="link-type"
+            v-if="!$route.meta.onlyView"
+            @click="handleUpdateUser(row)">
+            {{row.nickName}}
+          </div>
+          <div v-else>{{row.nickName}}</div>
+        </template>
       </el-table-column>
-      <el-table-column
+      <!-- <el-table-column
         label="Name"
         prop="nickName"
-        align="center"
-        class-name="fixed-width">
-          <template slot-scope="{row}">
-            <span>{{ row.nickName }}</span>
-          </template>
-      </el-table-column>
+        align="center"/> -->
       <el-table-column
         label="Credit"
         prop="credit"
-        align="center"
-        class-name="fixed-width"/>
+        align="center"/>
       <el-table-column
         label="Email"
         prop="email"
-        align="center"
-        class-name="fixed-width">
-          <template slot-scope="{row}">
-            <span>{{ row.email }}</span>
-          </template>
-      </el-table-column>
+        align="center"/>
       <el-table-column
         width="200"
         label="Phone"
         prop="phone"
-        align="center"
-        class-name="fixed-width">
-          <template slot-scope="{row}">
-            <span>{{ row.phone }}</span>
-          </template>
-      </el-table-column>
+        align="center"/>
       <el-table-column
         width="150"
         label="Status"
         prop="status"
-        align="center"
-        class-name="fixed-width">
-          <template slot-scope="{row}">
-            <span>{{ row.status }}</span>
-          </template>
-      </el-table-column>
+        align="center"/>
       <el-table-column
-        label="Actions"
+        label="Action"
         align="center"
-        width="240"
-        class-name="fixed-width">
-          <template slot-scope="{row, $index}">
-            <TableAction
-              @edit="handleUpdateUser(row, $index)"
-              @delete="handleDeleteUser(row, $index)"/>
-            <!-- <el-button
-              type="primary"
-              size="mini"
-              @click="handleUpdateUser(row, $index)">
-                Edit
-            </el-button>
-            <el-button
-              size="mini"
-              type="danger"
-              @click="handleDeleteUser(row, $index)">
-                Delete
-            </el-button> -->
-          </template>
+        width="210"
+        v-if="!$route.meta.onlyView">
+        <template slot-scope="{row, $index}">
+          <TableAction
+            @edit="handleUpdateUser(row, $index)"
+            @delete="handleDeleteUser(row, $index)"/>
+          <!-- <el-button
+            type="primary"
+            size="mini"
+            @click="handleUpdateUser(row, $index)">
+              Edit
+          </el-button>
+          <el-button
+            size="mini"
+            type="danger"
+            @click="handleDeleteUser(row, $index)">
+              Delete
+          </el-button> -->
+        </template>
       </el-table-column>
     </el-table>
     <div class="right">
@@ -128,16 +107,13 @@
         :limit.sync="listQuery.limit"
         @pagination="handlePageChange" />
     </div>
-    
   </div>
 </template>
 
 <script>
-
 import waves from '@/directive/waves' // waves directive
 import Pagination from '@/components/Pagination' // secondary package based on el-pagination
 import TableAction from '@/components/TableAction'
-
 import {
   fetchList,
   deleteUser,