Procházet zdrojové kódy

Add user management

vbea před 2 roky
rodič
revize
1eb5d26eee

+ 57 - 0
Strides-Admin/src/http/api/apiUser.js

@@ -0,0 +1,57 @@
+import {del, download, get, post, put} from '../http'
+
+const apiUser = {
+  getGroupTypeOptions() {
+    return get("group/group-type-select")
+  },
+  getGroupByType(type) {
+    return get("group/group-select", {groupType: type})
+  },
+  getUserTypeOptions() {
+    return get("device-user/user-types")
+  },
+  pageAppUsers(params) {
+    return post("device-user/user-pages", params)
+  },
+  getAppUserInfo(userPk) {
+    return get('device-user/users/' + userPk)
+  },
+  addAppUser(data) {
+    return post('device-user/users', data)
+  },
+  updateAppUser(data) {
+    return put('device-user/users', data)
+  },
+  getAppUserInfo(userPk) {
+    return get('device-user/users/' + userPk)
+  },
+  deleteAppUser(userPk) {
+    return del('device-user/users/' + userPk)
+  },
+  approveUserMembership(membershipId) {
+    return put('device-user/membership-pass/' + membershipId)
+  },
+  rejectUserMembership(membershipId) {
+    return put('device-user/membership-reject/' + membershipId)
+  },
+  deleteUserMembership(membershipId) {
+    return del('device-user/membership-delete/' + membershipId)
+  },
+  deleteUserVehicle(vehiclePk) {
+    return del('device-user/user-vehicles/' + vehiclePk)
+  },
+  downloadTemplate() {
+    return download('device-user/download-template')
+  },
+  userTopup(params) {
+    return post("device-user/manual-top-up", params)
+  },
+  pageChargeHistory(params) {
+    return post("device-user/charging-history", params)
+  },
+  pageTopupHistory(params) {
+    return post("device-user/top-up-history", params)
+  }
+}
+  
+export default apiUser;

+ 76 - 0
Strides-Admin/src/router/UserRouterV2.js

@@ -0,0 +1,76 @@
+import Layout from '@/layout'
+
+export default {
+  path: '/user-management',
+  component: Layout,
+  meta: {
+    title: 'User Management',
+    icon: 'user-management',
+    activeIcon: 'user-management-active',
+  },
+  children: [
+    {
+      path: '/user-management',
+      component: () => import('@/views/user/index'),
+      name: 'user-management',
+      meta: {
+        title: 'User Management',
+        breadcrumb: false
+        //icon: 'sidebar-submenu-item',
+        //activeIcon: 'sidebar-submenu-item-active'
+      }
+    },
+    {
+      path: '/user-management/blacklist',
+      component: () => import('@/views/driver/index'),
+      name: 'BlackList',
+      meta: {
+        title: 'Blacklist/Suspension',
+        icon: 'sidebar-submenu-item',
+        activeIcon: 'sidebar-submenu-item-active',
+      }
+    },
+    {
+      path: '/user-management/add',
+      component: () => import('@/views/user/detail'),
+      name: 'addUser',
+      hidden: true,
+      meta: {
+        title: 'Add User',
+        activeMenu: '/user-management'
+      }
+    },
+    {
+      path: '/user-management/update/:id',
+      component: () => import('@/views/user/detail'),
+      name: 'userDetail',
+      hidden: true,
+      meta: {
+        title: 'Edit User',
+        activeMenu: '/user-management'
+      }
+    },{
+      path: '/user-management/topup-history/:id',
+      component: () => import('@/views/user/hisTopUp'),
+      name: 'userDetail',
+      hidden: true,
+      meta: {
+        title: 'Top Up History',
+        icon: 'user-management',
+        affix: true,
+        activeMenu: '/user-management'
+      }
+    },{
+      path: '/user-management/charging-history/:id',
+      component: () => import('@/views/user/hisCharge'),
+      name: 'userDetail',
+      hidden: true,
+      meta: {
+        title: 'Charging History',
+        icon: 'user-management',
+        affix: true,
+        activeMenu: '/user-management'
+      }
+    }
+  ]
+}

+ 1 - 1
Strides-Admin/src/router/index.js

@@ -3,7 +3,7 @@ import VueRouter from 'vue-router'
 import Layout from '@/layout'
 import SiteRouter from './SiteRouter'
 import ChargeRouter from './ChargeRouter'
-import UserRouter from './UserRouter'
+import UserRouter from './UserRouterV2'
 import PartnershipRouter from './PartnershipRouter'
 import EnergyRouter from './EnergyRouter'
 import OCPPRouter from './OCPPRouter'

+ 31 - 4
Strides-Admin/src/styles/index.scss

@@ -186,14 +186,41 @@ aside {
 .link-type:focus {
   color: #337ab7;
   cursor: pointer;
+  position: relative;
   transition: all .3s;
-
-  &:hover {
-    color: rgb(32, 160, 255);
-    text-decoration: underline;
+  
+  &::before {
+    top: auto;
+    left: 0px;
+    right: 0px;
+    bottom: 0px;
+    height: 1px;
+    content: " ";
+    position: absolute;
+    transform: scaleX(0);
+    transition: all .2s;
+    background-color: #3179E4;
+    backface-visibility: hidden;
+  }
+  
+  &:hover::before {
+    transform: scaleX(1);
   }
 }
 
+.link-detail {
+  color: #337ab7;
+  cursor: pointer;
+  transition: all .3s;
+  text-decoration: none;
+}
+
+.link-detail:active,
+.link-detail:hover {
+  color: #ff5500;
+  text-decoration: underline;
+}
+
 .underline {
   cursor: pointer;
   text-decoration: underline;

+ 11 - 6
Strides-Admin/src/views/access/index.vue

@@ -33,7 +33,8 @@
       fit>
       <el-table-column
         label="Name"
-        align="center">
+        align="center"
+        min-width="100">
           <template slot-scope="{row}">
             <span>{{ row.userName }}</span>
           </template>
@@ -41,15 +42,18 @@
       <el-table-column
         label="Email Address"
         align="center"
-        prop="email"/>
+        prop="email"
+        min-width="120"/>
       <el-table-column
         label="Role"
         align="center"
-        prop="roleName"/>
+        prop="roleName"
+        min-width="100"/>
       <el-table-column
         label="Description"
         align="center"
-        prop="desc">
+        prop="desc"
+        min-width="150">
         <template slot-scope="{row}">
           <div
             v-for="item in row.desc"
@@ -62,11 +66,12 @@
       <el-table-column
         label="Last Login"
         align="center"
-        prop="lastLogin"/>
+        prop="lastLogin"
+        min-width="120"/>
       <el-table-column
         label="Action"
         align="center"
-        min-width="140">
+        min-width="120">
         <template slot-scope="{row}">
           <TableAction
             @edit="updateUser(row)"

+ 15 - 9
Strides-Admin/src/views/charging/ConfigureStations.vue

@@ -24,36 +24,42 @@
       <el-table-column
         label="Station ID"
         align="center"
-        prop="chargeBoxId"/>
+        prop="chargeBoxId"
+        min-width="100"/>
       <el-table-column
         label="Connector ID"
         align="center"
-        prop="connectorId"/>
+        prop="connectorId"
+        min-width="120"/>
       <el-table-column
         label="Site Name"
         align="center"
-        prop="siteName"/>
+        prop="siteName"
+        min-width="100"/>
       <el-table-column
         label="Description"
         align="center"
-        prop="description">
+        prop="description"
+        min-width="120">
         <template slot-scope="{row}">
-          <div
-            class="underline"
+          <span
+            class="link-type"
             v-if="row.description"
             @click="viewProfiles(row)">
             {{row.description}}
-          </div>
+          </span>
         </template>
       </el-table-column>
       <el-table-column
         label="Validity Period"
         align="center"
-        prop="validityPeriod"/>
+        prop="validityPeriod"
+        min-width="120"/>
       <el-table-column
         label="Action"
         align="center"
-        prop="description">
+        prop="description"
+        min-width="80">
         <template slot-scope="{row}">
           <el-dropdown
             class="action-dropdown"

+ 2 - 2
Strides-Admin/src/views/company/index.vue

@@ -43,12 +43,12 @@
         min-width="200"
         fixed="left">
         <template v-slot="{ row }">
-          <div
+          <span
             class="link-type"
             v-if="!$route.meta.onlyView"
             @click="onClickEditButton(row)">
             {{row.groupName}}
-          </div>
+          </span>
           <div v-else>{{row.groupName}}</div>
         </template>
       </el-table-column>

+ 5 - 26
Strides-Admin/src/views/feedback/FeedbackManagement.vue

@@ -73,7 +73,7 @@
         align="center"
         class-name="fixed-width">
           <template slot-scope="{row}">
-            <span>{{ row.readStatus ? "Read" : "Unread" }}</span>
+            <span :class="{readYes: row.readStatus}">{{ row.readStatus ? "Read" : "Unread" }}</span>
           </template>
       </el-table-column>
       <el-table-column
@@ -188,30 +188,9 @@
   }
 </script>
 
-<style scoped="scoped">
-  .link-detail {
-    color: #2370ff;
-    margin-left: 4px;
-    text-decoration: none;
-  }
-  
-  .link-detail:active,
-  .link-detail:hover {
-    color: #ff5500;
-    text-decoration: underline;
-  }
-  
-  .el-table >>> .cell {
-    color: #333;
-  }
-  
-  .el-table >>> .readYes td {
-    background-color: #eee !important;
-  }
-  .el-table >>> .readYes .cell {
-    color: #999;
-  }
-  .el-table >>> .readYes .link-detail {
-    color: #999;
+<style lang="scss" scoped="scoped">
+  @import '../../styles/variables.scss';
+  .readYes {
+    color: $panGreen;
   }
 </style>

+ 16 - 8
Strides-Admin/src/views/financial/IdleFee.vue

@@ -65,35 +65,43 @@
       <el-table-column
         label="User ID"
         align="center"
-        prop="userPk"/>
+        prop="userPk"
+        min-width="100"/>
       <el-table-column
         label="Site Name"
         align="center"
-        prop="siteName"/>
+        prop="siteName"
+        min-width="100"/>
       <el-table-column
         label="Station ID"
         align="center"
-        prop="chargeBoxId"/>
+        prop="chargeBoxId"
+        min-width="100"/>
       <el-table-column
         label="Start Date/Time"
         align="center"
-        prop="startTime"/>
+        prop="startTime"
+        min-width="130"/>
       <el-table-column
         label="End Date/Time"
         align="center"
-        prop="endTime"/>
+        prop="endTime"
+        min-width="130"/>
       <el-table-column
         label="Duration"
         align="center"
-        prop="duration"/>
+        prop="duration"
+        min-width="100"/>
       <el-table-column
         label="Rate"
         align="center"
-        prop="rate"/>
+        prop="rate"
+        min-width="200"/>
       <el-table-column
         label="Charges"
         align="center"
-        prop="charges"/>
+        prop="charges"
+        min-width="100"/>
     </el-table>
     <div class="right">
       <Pagination

+ 18 - 10
Strides-Admin/src/views/financial/TopUp.vue

@@ -36,18 +36,20 @@
       <el-table-column
         label="Transaction ID"
         align="center"
-        prop="creditHistoryPk"/>
+        prop="creditHistoryPk"
+        min-width="120"/>
       <el-table-column
         label="User ID"
         align="center"
-        width="80">
+        min-width="80">
           <template slot-scope="{row}">
             <span>{{ row.userPk }}</span>
           </template>
       </el-table-column>
       <el-table-column
         label="Email"
-        align="center">
+        align="center"
+        min-width="100">
         <template slot-scope="{row}">
           <span :title="row.email">{{ row.email }}</span>
         </template>
@@ -55,28 +57,33 @@
       <el-table-column
         label="Platform"
         align="center"
-        prop="paymentPlatform"/>
+        prop="paymentPlatform"
+        min-width="100"/>
       <el-table-column
         label="Top Up Type"
         align="center"
-        prop="payType"/>
+        prop="payType"
+        min-width="120"/>
       <el-table-column
         label="Amount"
         align="center"
-        prop="amount"/>
+        prop="amount"
+        min-width="100"/>
       <el-table-column
         label="Creation Time"
         align="center"
-        prop="creationTime"/>
+        prop="creationTime"
+        min-width="120"/>
       <el-table-column
         label="Payment Status"
         align="center"
-        prop="paymentStatus"/>
+        prop="paymentStatus"
+        min-width="130"/>
       <el-table-column
         label="Payment Reference"
         align="center"
         prop="paymentReference"
-        width="150px">
+        min-width="150">
         <template v-slot="{ row }">
           <div @dblclick="copyReference(row.paymentReference)">
             <el-input
@@ -94,7 +101,8 @@
       <el-table-column
         label="Update Time"
         align="center"
-        prop="updateTime"/>
+        prop="updateTime"
+        min-width="120"/>
     </el-table>
     <div class="right">
       <Pagination

+ 16 - 8
Strides-Admin/src/views/financial/index.vue

@@ -65,35 +65,43 @@
       <el-table-column
         label="Transaction ID"
         align="center"
-        prop="transactionPk"/>
+        prop="transactionPk"
+        min-width="120"/>
       <el-table-column
         label="Site Name"
         align="center"
-        prop="siteName"/>
+        prop="siteName"
+        min-width="100"/>
       <el-table-column
         label="Station ID"
         align="center"
-        prop="chargeBoxId"/>
+        prop="chargeBoxId"
+        min-width="100"/>
       <el-table-column
         label="Start Date/Time"
         align="center"
-        prop="startDateTime"/>
+        prop="startDateTime"
+        min-width="130"/>
       <el-table-column
         label="End Date/Time"
         align="center"
-        prop="endDateTime"/>
+        prop="endDateTime"
+        min-width="120"/>
       <el-table-column
         label="Power (kwh)"
         align="center"
-        prop="power"/>
+        prop="power"
+        min-width="120"/>
       <el-table-column
         label="Rate"
         align="center"
-        prop="rate"/>
+        prop="rate"
+        min-width="200"/>
       <el-table-column
         label="Charges"
         align="center"
-        prop="charges"/>
+        prop="charges"
+        min-width="100"/>
     </el-table>
     <div class="right">
       <Pagination

+ 2 - 2
Strides-Admin/src/views/limit/CreditLimit.vue

@@ -33,13 +33,13 @@
       <el-table-column
         label="Monthly Credit Limit"
         align="center"
-        min-width="140">
+        min-width="160">
         <template slot-scope="{row}">{{ row.creditLimitAmount }}</template>
       </el-table-column>
       <el-table-column
         label="Remaining Credit Limit"
         align="center"
-        min-width="140">
+        min-width="180">
         <template slot-scope="{row}">{{ row.creditLimitRemainingAmount }}</template>
       </el-table-column>
       <el-table-column

+ 2 - 2
Strides-Admin/src/views/notification/index.vue

@@ -22,11 +22,11 @@
         label="Notificaiton ID"
         min-width="180">
         <template v-slot="{row}">
-          <div
+          <span
             class="link-type"
             @click="viewNotification(row)">
             {{ row.notificationId }}
-          </div>
+          </span>
         </template>
       </el-table-column>
       <el-table-column

+ 8 - 11
Strides-Admin/src/views/transaction/reservations.vue

@@ -31,10 +31,9 @@
       fit
       style="width: 100%;">
       <el-table-column
-        width="100"
         label="User ID"
         align="center"
-        class-name="fixed-width">
+        min-width="100">
           <template slot-scope="{row}">
             <span>{{ row.userPk }}</span>
           </template>
@@ -42,7 +41,7 @@
       <el-table-column
         label="Name"
         align="center"
-        class-name="fixed-width">
+        min-width="100">
           <template slot-scope="{row}">
             <span>{{ row.userName }}</span>
           </template>
@@ -50,16 +49,15 @@
       <el-table-column
         label="Email"
         align="center"
-        class-name="fixed-width">
+        min-width="100">
           <template slot-scope="{row}">
             <span>{{ row.email }}</span>
           </template>
       </el-table-column>
       <el-table-column
-        width="200"
         label="Phone"
         align="center"
-        class-name="fixed-width">
+        min-width="100">
           <template slot-scope="{row}">
             <span>{{ row.mobile }}</span>
           </template>
@@ -67,25 +65,24 @@
       <el-table-column
         label="Vehicle License Plate"
         align="center"
-        class-name="fixed-width">
+        class-name="fixed-width"
+        min-width="180">
           <template slot-scope="{row}">
             <span>{{ row.mobile }}</span>
           </template>
       </el-table-column>
       <el-table-column
-        width="150"
         label="Status"
         align="center"
-        class-name="fixed-width">
+        min-width="100">
           <template slot-scope="{row}">
             <span>{{ row.status }}</span>
           </template>
       </el-table-column>
       <el-table-column
-        width="210"
         label="Action"
         align="center"
-        class-name="fixed-width">
+        min-width="120">
           <template slot-scope="{row}">
             <TableAction/>
           </template>

+ 8 - 8
Strides-Admin/src/views/transaction/transactions.vue

@@ -55,16 +55,16 @@
       class="no-border"
       style="width: 100%;min-height: 65vh;">
       <el-table-column
-        min-width="100"
+        min-width="120"
         label="Transaction ID"
         align="center"
         class-name="fixed-width">
         <template slot-scope="{row}">
-          <div
+          <span
             class="link-type"
             @click="viewTransiction(row)">
             {{ row.transactionPk }}
-          </div>
+          </span>
         </template>
       </el-table-column>
       <!--el-table-column
@@ -119,7 +119,7 @@
         min-width="120">
         <template slot-scope="{row}">
           <div
-            class="link-type"
+            class="link-detail"
             @click="viewTransiction(row)"
             :title="row.chargeBoxId">
             {{ row.chargeBoxId }}
@@ -137,7 +137,7 @@
       <el-table-column
         label="Start Date/Time"
         align="center"
-        min-width="120">
+        min-width="140">
         <template slot-scope="{row}">
           <span :title="row.startDateTime">{{ row.startDateTime }}</span>
         </template>
@@ -145,13 +145,13 @@
       <el-table-column
         label="End Date/Time"
         align="center"
-        min-width="120">
+        min-width="140">
         <template slot-scope="{row}">
           <span :title="row.endDateTime">{{ row.endDateTime  }}</span>
         </template>
       </el-table-column>
       <el-table-column
-        label="Power (kWH)"
+        label="Power (kWh)"
         align="center"
         min-width="120">
         <template slot-scope="{row}">
@@ -161,7 +161,7 @@
       <el-table-column
         label="Rate"
         align="center"
-        min-width="130">
+        min-width="150">
         <template slot-scope="{row}" v-if="row.rate">
           <span
             v-for="(item, idx) in row.rate.split(';')"

+ 1 - 1
Strides-Admin/src/views/user/UserDetail.vue

@@ -228,7 +228,7 @@ import {
 import { mapState } from 'vuex'
 import site from '../../http/api/site'
 import {getCountryList} from '../../utils/index.js'
-import TopUp from './TopUp.vue'
+import TopUp from './views/TopUp.vue'
 import settings from '../../settings.js'
 export default {
   name: 'UserDetial',

+ 561 - 0
Strides-Admin/src/views/user/detail.vue

@@ -0,0 +1,561 @@
+<template>
+  <div class="container" v-loading="loading">
+    <el-form
+      ref="userRef"
+      :model="userForm"
+      :rules="rules"
+      label-width="140px"
+      label-position="right">
+      <div class="content">
+        <div class="section-title">PROFILE DETAILS</div>
+        <el-row :gutter="20">
+          <el-col :xs="24" :md="12">
+            <el-form-item
+              prop='nickName'
+              label="Display Name:"
+              class="flex1">
+              <el-input
+                class="input-text"
+                v-model="userForm.nickName"/>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :md="12">
+            <el-form-item
+              label="Phone Number:"
+              prop="phone"
+              class="flex1">
+              <div class="input-text flexc">
+                <el-select
+                  v-model="userForm.callingCode"
+                  class="area-code">
+                  <el-option
+                    v-for="areaCode in options.callingCode"
+                    :label="'+'+areaCode.callingCode"
+                    :value="areaCode.callingCode"
+                    :key="areaCode.callingCode" />
+                </el-select>
+                <el-input
+                  class="phone-number"
+                  v-model="userForm.phone" />
+              </div>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :md="12">
+            <el-form-item
+              label="Email Address:"
+              prop="email"
+              class="flex1">
+              <el-input
+                class="input-text"
+                v-model="userForm.email" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <div class="section-title">Address</div>
+        <el-row :gutter="20">
+          <el-col :xs="24" :md="12">
+            <el-form-item
+              label="Country:">
+              <el-select
+                v-model="userForm.address.country"
+                class="input-text">
+                <el-option
+                  v-for="item in options.country"
+                  :key="item.name"
+                  :label="item.name"
+                  :value="item.value" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :md="12">
+            <el-form-item
+              label="Street:">
+              <el-input
+                class="input-text"
+                v-model="userForm.address.street" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :xs="24" :md="12">
+            <el-form-item
+              label="City:">
+              <el-input
+                class="input-text"
+                v-model="userForm.address.city" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :md="12">
+            <el-form-item
+              label="Postal Code:">
+              <el-input
+                class="input-text"
+                v-model="userForm.address.zipCode" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+      <div class="content">
+        <div class="section-title">ACCOUNT DETAILS</div>
+        <el-row :gutter="20">
+          <el-col :xs="24" :md="12">
+            <el-form-item
+              label="Account Type:"
+              class="flex1">
+              <el-select
+                v-model="userForm.accountType"
+                class="input-text">
+                <el-option
+                  v-for="item in options.userType"
+                  :key="item"
+                  :label="item"
+                  :value="item" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <template v-if="isEdit">
+            <el-col :xs="24" :md="12">
+              <el-form-item
+                label="Charging History:"
+                class="flex1">
+                <div class="flexc input-text">
+                  <el-input
+                    class="input-text-1"
+                    v-model="userForm.chargingCount"
+                    readonly/>
+                  <el-button
+                    type="primary"
+                    @click="toHistoryCharge">
+                    VIEW HISTORY
+                  </el-button>
+                </div>
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :md="12">
+              <el-form-item
+                label="Credit Amount:">
+                <div class="flexc input-text">
+                  <el-input
+                    class="input-text-1"
+                    :value="userForm.currencySymbol + ' ' + userForm.credit"
+                    readonly/>
+                  <el-button
+                    type="success"
+                    @click="showTopupModal">
+                    TOP UP
+                  </el-button>
+                </div>
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :md="12">
+              <el-form-item
+                label="Top-up History:"
+                class="flex1">
+                <div class="flexc input-text">
+                  <el-input
+                    class="input-text-1"
+                    v-model="userForm.topUpCount"
+                    readonly/>
+                  <el-button
+                    type="primary"
+                    @click="toHistoryTopup">
+                    VIEW HISTORY
+                  </el-button>
+                </div>
+              </el-form-item>
+            </el-col>
+          </template>
+        </el-row>
+      </div>
+      <div class="content">
+        <div class="section-title">VEHICLE DETIALS</div>
+        <ListVehicle v-model="userForm.vehicles"/>
+      </div>
+      <div
+        class="content"
+        v-show="userForm.accountType == 'MEMBER'">
+        <div class="section-title">Memberships</div>
+        <ListMember
+          v-model="userForm.members"/>
+      </div>
+      <div
+        class="content"
+        v-show="userForm.accountType == 'FLEET'">
+        <div class="section-title">Fleet</div>
+        <ListMember
+          v-model="userForm.fleets"
+          :isFleet="true"/>
+      </div>
+      <div class="content flexcr">
+        <div class="buttons">
+          <el-button
+            class="cancel-button"
+            type="primary"
+            @click="handleClickCancleButton">
+              Cancel
+          </el-button>
+          <el-button
+            class="confirm-button"
+            type="primary"
+            @click="handleClickConfirmButton">
+              Save
+          </el-button>
+        </div>
+        <div class="update-by" v-if="isEdit">
+          <span
+            class="add-text"
+            :title='"CREATED BY " + userForm.createdBy + " ON " + userForm.createdOn'>
+            LAST UPDATED BY {{userForm.updatedBy}} TIMESTAMP: {{userForm.updatedOn}}
+          </span>
+        </div>
+      </div>
+    </el-form>
+    <TopUp
+      :visible="showTopup"
+      :userInfo="userForm"
+      @hide="hideTopupModal"
+      @success="hideTopupModal(true)"/>
+  </div>
+</template>
+
+<script>
+import api from '@/http/api/apiUser'
+import site from '../../http/api/site'
+import {getCountryList} from '../../utils/index.js'
+import TopUp from './views/TopUp.vue'
+import settings from '../../settings.js'
+import ListMember from './views/ListMember.vue'
+import ListVehicle from './views/ListVehicle.vue'
+export default {
+  components: {ListMember,ListVehicle,TopUp},
+  data() {
+    return {
+      loading: false,
+      options: {
+        country: [],
+        userType: [],
+        callingCode: []
+      },
+      userForm: {
+        userPk: '',
+        email: '',
+        nickName: '',
+        phone: '',
+        callingCode: settings.defaultCalling,
+        countryCode: settings.defaultCountry,
+        accountType: "PUBLIC",
+        address: {
+          zipCode: '',
+          street: '',
+          city: '',
+          country: settings.defaultCountry,
+          addressPk: '',
+          houseNumber: ''
+        },
+        fleet: {
+          groupPk: "",
+          cardFront: "",
+          membershipNo: ""
+        },
+        fleets: [],
+        members: [],
+        vehicles: [],
+        chargingCount: "",
+        topUpCount: "",
+        credit: 0,
+        currencySymbol: "$"
+      },
+      rules: {
+        nickName: [{
+          required: true,
+          trigger: 'blur',
+          message: 'Display Name is required',
+        }],
+        email: [{
+          required: true,
+          trigger: 'blur',
+          message: 'Email is required',
+        }],
+        phone: [{
+          required: true,
+          trigger: 'blur',
+          message: 'Phone Number is required',
+        }],
+        address: {
+          zipCode: [{
+            required: true,
+            trigger: 'blur',
+            message: 'Postal Code is required',
+          }],
+          street: [{
+            required: true,
+            trigger: 'blur',
+            message: 'Street is required',
+          }],
+          city: [{
+            required: true,
+            trigger: 'blur',
+            message: 'City is required',
+          }],
+          country: [{
+            required: true,
+            trigger: 'blur',
+            message: 'Country is required',
+          }]
+        }
+      },
+      isEdit: false,
+      showTopup: false
+    }
+  },
+  mounted() {
+    this.loading = true;
+    this.getCountryList()
+    if (this.$route.params.id) {
+      this.isEdit = true;
+      this.getUserInfo()
+    }
+  },
+  methods: {
+    handleClickCancleButton() {
+      this.$router.replace({
+        path: "/user-management"
+      })
+    },
+    getCountryList() {
+      site.getCountryList().then(res => {
+        if (res.data) {
+          this.options.country = res.data
+        }
+      }).catch(err => {
+        this.$message({
+          message: error,
+          type: 'error'
+        })
+        this.loading = false;
+      })
+      getCountryList(list => {
+        this.options.callingCode = list
+      })
+      this.getUserTypeOption();
+    },
+    getUserTypeOption() {
+      api.getUserTypeOptions().then(res => {
+        if (res.data) {
+          this.options.userType = res.data
+        }
+      }).catch(err => {
+        this.$message({
+          message: error,
+          type: 'error'
+        })
+      }).finally(() => {
+        this.loading = false;
+      })
+    },
+    getUserInfo() {
+      api.getAppUserInfo(this.$route.params.id).then(res => {
+        if (res.data) {
+          this.userForm = res.data
+          this.userForm.fleets = []
+          if (res.data.fleet) {
+            this.userForm.fleets.push(res.data.fleet)
+          }
+          if (!res.data.address) {
+            this.userForm.address = {
+              zipCode: '',
+              street: '',
+              city: '',
+              country: settings.defaultCountry,
+              addressPk: '',
+              houseNumber: ''
+            }
+          }
+        }
+      }).catch(err => {
+        this.$message.error(err)
+      }).finally(() => {
+        this.loading = false;
+      })
+    },
+    handleClickConfirmButton() {
+      this.$refs.userRef.validate((valid) => {
+        if (valid) {
+          if (this.userForm.accountType == "FLEET") {
+            this.userForm.fleet = this.userForm.fleets[0]
+          } else {
+            this.userForm.fleet = null;
+          }
+          if (this.userForm.accountType != "MEMBER") {
+            this.userForm.members = []
+          }
+          var list = this.userForm.vehicles.filter(item => (item.brand && item.model && item.licensePlate))
+          if (list.length == 0) {
+            list = null;
+          }
+          this.userForm.vehicles = list;
+          if (this.isEdit) {
+            this.updateUser()
+          } else {
+            this.addUser();
+          }
+        }
+      })
+    },
+    updateUser() {
+      this.loading = true
+      api.updateAppUser(this.userForm).then(() => {
+        this.$message({
+          message: 'Update user successfully',
+          type: 'success'
+        })
+        this.handleClickCancleButton();
+      }).catch((error) => {
+        this.$message({
+          message: error,
+          type: 'error'
+        })
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    addUser() {
+      this.loading = true
+      api.addAppUser(this.userForm).then(() => {
+        this.$message({
+          message: 'Add user successfully',
+          type: 'success'
+        })
+        this.handleClickCancleButton();
+      }).catch((error) => {
+        this.$message({
+          message: error,
+          type: 'error'
+        })
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    showTopupModal() {
+      this.showTopup = true
+    },
+    hideTopupModal(success) {
+      this.showTopup = false;
+      if (success) {
+        this.getUserInfo()
+      }
+    },
+    toHistoryCharge() {
+      this.$router.push("/user-management/charging-history/" + this.$route.params.id)
+    },
+    toHistoryTopup() {
+      this.$router.push("/user-management/topup-history/" + this.$route.params.id)
+    }
+  }
+}
+</script>
+
+<style scoped lang='scss'>
+  @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;
+  }
+  .input-text {
+    width: 100%;
+    min-width: 150px;
+  }
+  .input-text ::v-deep .el-textarea__inner {
+    font-family: sans-serif;
+  }
+  
+  .input-text-1 {
+    flex: 1;
+    margin-right: 10px;
+  }
+  
+  .area-code {
+    width: 80px;
+    margin-right: 10px;
+  }
+  
+  .phone-number {
+    flex: 1;
+    width: 100%;
+  }
+  
+  .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: 800px) {
+    .container {
+      padding: 20px;
+    }
+    .content {
+      padding: 15px 30px;
+    }
+  }
+  @media screen and (max-width: 500px) {
+    .container {
+      padding: 0px;
+    }
+    .content {
+      padding: 15px 20px;
+    }
+  }
+</style>

+ 23 - 12
Strides-Admin/src/views/user/hisCharge.vue

@@ -18,7 +18,7 @@
         class-name="fixed-width"
         v-for="(item, index) in columns"
         :key="index"
-        :width="item.width"
+        :min-width="item.width"
         :prop="item.prop"/>
       </el-table>
     </el-table>
@@ -34,7 +34,7 @@
 </template>
 
 <script>
-import api from '@/http/api/userManagement'
+import api from '@/http/api/apiUser.js'
 import Pagination from '@/components/Pagination'
 export default {
   data() {
@@ -48,31 +48,40 @@ export default {
       },
       columns: [{
         prop: "transactionId",
-        label: "Transaction ID"
+        label: "Transaction ID",
+        width: 120
       },{
         prop: "vehicle",
-        label: "Vehicle"
+        label: "Vehicle",
+        width: 100
       },{
         prop: "chargeBoxId",
-        label: "Station ID"
+        label: "Station ID",
+        width: 120
       },{
         prop: "connectorId",
-        label: "Connector"
+        label: "Connector",
+        width: 90
       },{
         prop: "startDateTime",
-        label: "Start Date/Time"
+        label: "Start Date/Time",
+        width: 140
       },{
         prop: "endDateTime",
-        label: "End Date/Time"
+        label: "End Date/Time",
+        width: 140
       },{
         prop: "power",
-        label: "Power (kWh)"
+        label: "Power (kWh)",
+        width: 120
       },{
         prop: "rate",
-        label: "Rate"
+        label: "Rate",
+        width: 200
       },{
         prop: "charges",
-        label: "Charges"
+        label: "Charges",
+        width: 100
       }]
     }
   },
@@ -84,7 +93,9 @@ export default {
   },
   methods: {
     goBack() {
-      this.$router.push({ path: '/user-management/update-user/' + this.$route.params.id })
+      this.$router.go(-1);
+      //this.$router.push({ path: '/user-management'})
+      //this.$router.push({ path: '/user-management/update-user/' + this.$route.params.id })
     },
     getTableData() {
       this.loading = true;

+ 19 - 10
Strides-Admin/src/views/user/hisTopUp.vue

@@ -18,7 +18,7 @@
         class-name="fixed-width"
         v-for="(item, index) in columns"
         :key="index"
-        :width="item.width"
+        :min-width="item.width"
         :prop="item.prop"/>
       </el-table>
     </el-table>
@@ -34,7 +34,7 @@
 </template>
 
 <script>
-import api from '@/http/api/userManagement'
+import api from '@/http/api/apiUser.js'
 import Pagination from '@/components/Pagination'
 export default {
   data() {
@@ -48,25 +48,32 @@ export default {
       },
       columns: [{
         prop: "transactionId",
-        label: "Transaction ID"
+        label: "Transaction ID",
+        width: 120
       },{
         prop: "topUpType",
-        label: "Top Up Type"
+        label: "Top Up Type",
+        width: 120
       },{
         prop: "amount",
-        label: "Amount"
+        label: "Amount",
+        width: 80
       },{
         prop: "creationTime",
-        label: "Creation Time"
+        label: "Creation Time",
+        width: 140
       },{
         prop: "paymentStatus",
-        label: "Payment Status"
+        label: "Payment Status",
+        width: 130
       },{
         prop: "paymentReference",
-        label: "Payment Reference"
+        label: "Payment Reference",
+        width: 200
       },{
         prop: "updateTime",
-        label: "Update Time"
+        label: "Update Time",
+        width: 120
       }]
     }
   },
@@ -78,7 +85,9 @@ export default {
   },
   methods: {
     goBack() {
-      this.$router.push({ path: '/user-management/update-user/' + this.$route.params.id })
+      this.$router.go(-1);
+      //this.$router.push({ path: '/user-management'})
+      //this.$router.push({ path: '/user-management/update-user/' + this.$route.params.id })
     },
     getTableData() {
       this.loading = true;

+ 432 - 0
Strides-Admin/src/views/user/index.vue

@@ -0,0 +1,432 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <div class="filter-view">
+        <el-select
+          class="filter-select"
+          v-model="filters.pageVo.userType"
+          placeholder="All User Type"
+          @change="onClickSearch"
+          clearable>
+          <el-option
+            v-for="item in userTypes"
+            :key="item"
+            :label="item"
+            :value="item" />
+        </el-select>
+        <el-select
+          class="filter-select"
+          v-model="filters.pageVo.accountStatus"
+          placeholder="All Account Status"
+          @change="onClickSearch"
+          clearable>
+          <el-option
+            label="Active"
+            value="A" />
+          <el-option
+            label="Inactive"
+            value="I" />
+        </el-select>
+        <el-input
+          class="filter-input"
+          v-model="filters.pageVo.criteria"
+          placeholder="Email, Name, License Plate"
+          @change="onClickSearch"
+          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">
+            Template
+          </el-button>
+        </my-upload>
+        <div v-if="!$route.meta.onlyView">
+          <el-button
+            icon="el-icon-download"
+            type="primary"
+            :loading="loading.download"
+            @click="onDownloadTmp">
+            Template
+          </el-button>
+        </div>
+        <div v-if="!$route.meta.onlyView">
+          <el-button
+            icon="el-icon-plus"
+            type="primary"
+            @click="onAddUser">
+            Add User
+          </el-button>
+        </div>
+      </div>
+    </div>
+    <el-table
+      :data="table.list"
+      class="no-border"
+      v-loading="loading.table">
+      <el-table-column
+        align="center"
+        label="Name"
+        prop="userPk"
+        min-width="150">
+        <template slot-scope="{row}">
+          <span
+            class="link-type"
+            @click="onEditUser(row)"
+            v-if="!$route.meta.onlyView">
+            {{ row.nickName }}
+          </span>
+          <span v-else>{{ row.nickName }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="E-mail"
+        prop="email"
+        min-width="160"/>
+      <el-table-column
+        align="center"
+        label="Vehicle"
+        prop="licensePlates"
+        min-width="120">
+        <template slot-scope="{row}">
+          <div v-for="item in row.licensePlates" :key="item">{{item}}</div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="Credits"
+        prop="credit"
+        min-width="100"/>
+      <el-table-column
+        min-width="140"
+        align="center"
+        label="User Type"
+        prop="userType">
+        <template slot-scope="{row}">
+          <span>{{row.userType || "Membership"}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="Group Name"
+        prop="fleetName"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Group Status"
+        prop="fleetStatus"
+        min-width="120"/>
+      <el-table-column
+        width="150"
+        align="center"
+        label="Membership"
+        prop="phone"
+        min-width="120">
+        <template slot-scope="{row}">
+          <div v-for="(item,index) in row.memberships" :key="index">
+            <span :class='"status-" + item.membershipStatus'>{{item.membershipName}}</span>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="Account Status"
+        prop="accountStatus"
+        min-width="140"/>
+      <el-table-column
+        align="center"
+        label="Action"
+        min-width="80"
+        v-if="!$route.meta.onlyView">
+        <template slot-scope="{ row }" v-if="row.accountStatus != 'Inactive'">
+          <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="onEditUser">
+                Edit
+              </el-dropdown-item>
+              <el-dropdown-item
+                command="showTopupModal">
+                Top Up Credits
+              </el-dropdown-item>
+              <el-dropdown-item
+                command="toHistoryTopup">
+                Top Up History
+              </el-dropdown-item>
+              <el-dropdown-item
+                command="toHistoryCharge">
+                Charging History
+              </el-dropdown-item>
+              <el-dropdown-item
+                command="onDeleteUser">
+                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>
+    <TopUp
+      :visible="actionTopup.visible"
+      :userInfo="actionTopup.row"
+      @hide="showTopupModal"
+      @success="showTopupModal('', true)"/>
+  </div>
+</template>
+
+<script>
+import Pagination from '@/components/Pagination'
+import TableAction from '@/components/TableAction'
+import MyUpload from '@/components/MyUpload'
+import TopUp from './views/TopUp.vue'
+import api from '@/http/api/apiUser'
+import { baseURL } from '@/http/http'
+import { getToken } from '@/utils/auth'
+export default {
+  data() {
+    return {
+      filters: {
+        pageNo: 1,
+        pageSize: 10,
+        pageVo: {
+          criteria: "",
+          userType: "",
+          accountStatus: ""
+        }
+      },
+      table: {
+        list: [],
+        total: 0
+      },
+      loading: {
+        table: false,
+        upload: false,
+        download: false
+      },
+      fileList: [],
+      userTypes: [],
+      actionTopup: {
+        row: {},
+        visible: false
+      }
+    }
+  },
+  computed: {
+    action() {
+      return baseURL + process.env.VUE_APP_API_PREFIX + '/device-user/batch-create'
+    },
+    headers() {
+      return {
+        accessToken: getToken()
+      }
+    }
+  },
+  components: {
+    Pagination,
+    TableAction,
+    MyUpload,
+    TopUp
+  },
+  created() {
+    this.getUserTypeOption()
+    this.onClickSearch()
+  },
+  methods: {
+    onClickSearch() {
+      this.filters.pageNo = 1
+      this.getTableData()
+    },
+    getUserTypeOption() {
+      api.getUserTypeOptions().then(res => {
+        if (res.data) {
+          this.userTypes = res.data
+        }
+      }).catch(err => {
+        this.$message({
+          message: error,
+          type: 'error'
+        })
+      })
+    },
+    getTableData() {
+      this.loading.table = true;
+      api.pageAppUsers(this.filters).then(res => {
+        if (res.data && res.total) {
+          this.table.total = res.total;
+          this.table.list = res.data;
+        } else {
+          this.table.total = 0;
+          this.table.list = [];
+        }
+      }).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/add' })
+    },
+    handleCommand(cb, item) {
+      this[cb](item)
+    },
+    showTopupModal(row, success) {
+      if (row && row.userPk) {
+        this.actionTopup.row = row;
+        this.actionTopup.visible = true
+      } else {
+        this.actionTopup.id = "";
+        this.actionTopup.visible = false;
+        if (success) this.getTableData();
+      }
+    },
+    toHistoryCharge(row) {
+      this.$router.push("/user-management/charging-history/" + row.userPk)
+    },
+    toHistoryTopup(row) {
+      this.$router.push("/user-management/topup-history/" + row.userPk)
+    },
+    onEditUser(row) {
+      this.$router.push({
+        path: "/user-management/update/" + row.userPk
+      })
+    },
+    onDeleteUser(row) {
+      this.$confirm('Are you sure you want to delete this user?', 'Delete', {
+        confirmButtonText: 'OK',
+        cancelButtonText: 'Cancel',
+        type: 'warning'
+      }).then(res => {
+        this.deleteUser(row.userPk);
+      })
+    },
+    deleteUser(id) {
+      this.loading.table = true;
+      api.deleteAppUser(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>
+  @import '../../styles/variables.scss';
+  .filter-input {
+    min-width: 100px;
+    max-width: 300px;
+  }
+  .filter-select {
+    min-width: 100px;
+    max-width: 180px;
+  }
+  .status-Pending {
+    color: $yellow;
+  }
+  .status-Pass {
+    color: $green;
+  }
+  .status-Reject {
+    color: $red;
+  }
+</style>

+ 377 - 0
Strides-Admin/src/views/user/views/ListMember.vue

@@ -0,0 +1,377 @@
+<template>
+  <el-table
+    :data="itemList"
+    class="user-sub-item-table no-border">
+    <el-table-column
+      label="Card No:"
+      min-width="90px">
+      <template slot-scope="{row}">
+        <el-input
+          class="item-input"
+          v-model="row.membershipNo" />
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="Group:"
+      min-width="90">
+      <template slot-scope="{row}">
+        <el-select
+          class="item-input"
+          v-model="row.groupPk">
+          <el-option
+            v-for="(item,index) in groupList"
+            :key="index"
+            :label="item.name"
+            :value="item.value"/>
+        </el-select>
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="Card Photo:"
+      min-width="90">
+      <template slot-scope="{$index,row}">
+        <el-upload
+          v-if="isEdit"
+          class="card-uploader"
+          accept=".jpg,.jpeg,.png,.gif,.JPG,.JPEG"
+          :action="action"
+          :headers="headers"
+          :show-file-list="false"
+          :on-success="e => onUploadSuccess(e, $index)"
+          :on-error="onUploadError"
+          :before-upload="e => beforeUpload(e, $index)">
+          <div
+            v-loading="loading.upload && $index == loading.index"
+            class="uploader-image">
+            <el-image
+              class="uploader-image"
+              v-if="row.cardFront"
+              :src="getImageSrc(row.cardFront)"/>
+            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+          </div>
+        </el-upload>
+        <div class="card-uploader" v-else-if="row.cardFront">
+          <el-image
+            class="uploader-image"
+            :src="getImageSrc(row.cardFront)"
+            :preview-src-list="previewSrcList(row.cardFront)"/>
+        </div>
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="Status:"
+      min-width="80">
+      <template slot-scope="{$index,row}">
+        <div class="flexc" v-loading="loading.action && $index == loading.index">
+          <span>{{row.membershipStatus}}</span>
+          <div class="flexl" v-if="row.membershipStatus=='Pending'">
+            <div
+              class="action-approve"
+              @click="approveItem($index)">Approve</div>
+            <div
+              class="action-reject"
+              @click="rejectItem($index)">Reject</div>
+          </div>
+        </div>
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="Action:"
+      min-width="100px">
+      <template slot-scope="{$index,row}">
+        <div v-loading="loading.subt">
+          <img
+            class="list-item-icon"
+            @click="onSubtItem($index)"
+            src="../../../assets/form-list-sub.png"/>
+          <img
+            v-if="$index === itemList.length - 1"
+            class="list-item-icon"
+            @click="onAddItem"
+            src="../../../assets/form-list-add.png"/>
+        </div>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+import api from '@/http/api/apiUser'
+import group from '@/http/api/group'
+import { baseURL } from '@/http/http'
+import { getToken } from '@/utils/auth'
+export default {
+  name: "ListMember",
+  props: {
+    list: {
+      type: Array,
+      default: () => []
+    },
+    isEdit: {
+      type: Boolean,
+      default: true
+    },
+    isFleet: {
+      type: Boolean,
+      default: false
+    }
+  },
+  model: {
+    prop: 'list',
+    event: 'change'
+  },
+  data() {
+    return {
+      loading: {
+        subt: false,
+        upload: false,
+        action: false,
+        index: -1
+      },
+      itemList: [{
+        membershipId: "",
+        membershipNo: "",
+        groupPk: "",
+        groupName: "",
+        cardFront: "",
+        cardReverse: "",
+        membershipStatus: ""
+      }],
+      groupList: []
+    };
+  },
+  watch: {
+    list: {
+      deep: true,
+      handler(n, o) {
+        this.itemList = n || [];
+        if (this.itemList.length === 0) {
+          this.onAddItem();
+        } else {
+          this.onChange()
+        }
+      }
+    }
+  },
+  computed: {
+    action() {
+      return baseURL + process.env.VUE_APP_API_PREFIX + '/picture/upload'
+    },
+    headers() {
+      return {
+        accessToken: getToken(),
+        photoSubDir: "MEMBERSHIP",
+      }
+    },
+    previewSrcList(path) {
+      const imageSrc = []
+      if (path) {
+        imageSrc.push(baseURL + path)
+      }
+      return imageSrc
+    }
+  },
+  mounted() {
+    this.getGroupOptions();
+  },
+  methods: {
+    onChange() {
+      this.$emit('change', this.itemList);
+    },
+    getImageSrc(path) {
+      return baseURL + path
+    },
+    getGroupOptions() {
+      api.getGroupByType(this.isFleet ? "FLEET" : "MEMBER").then(res => {
+        if (res.data) {
+          this.groupList = res.data
+        }
+      }).catch(err => {
+        this.$message({
+          message: err,
+          type: 'error'
+        })
+      })
+    },
+    beforeUpload(file, index) {
+      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;
+      this.loading.index = index;
+    },
+    onUploadSuccess(response, index) {
+      this.itemList[index].cardFront = response.data.picturePath;
+      this.loading.upload = false;
+      this.onChange();
+    },
+    onUploadError(err) {
+      this.$message({
+        type: 'error',
+        message: "" + err
+      });
+      this.loading.upload = false;
+    },
+    onAddItem() {
+      this.itemList.push({
+        membershipId: "",
+        membershipNo: "",
+        groupPk: "",
+        groupName: "",
+        cardFront: "",
+        cardReverse: "",
+        membershipStatus: ""
+      })
+      this.onChange();
+    },
+    onSubtItem(index) {
+      let item = this.itemList[index]
+      if (item.membershipId) {
+        this.$confirm("Are you sure you want to delete this vehicle ?", "Delete", {
+          confirmButtonText: 'OK',
+          cancelButtonText: 'Cancel',
+          type: 'warning'
+        }).then(() => {
+          this.loading.subt = true
+          this.deleteItem(item.membershipId, index);
+        })
+      } else {
+        this.itemList.splice(index, 1)
+        if (this.itemList.length === 0) {
+          this.onAddItem();
+        }
+      }
+    },
+    deleteItem(id, index) {
+      api.deleteUserMembership(id).then(() => {
+        this.itemList.splice(index, 1);
+        if (this.itemList.length === 0) {
+          this.onAddItem();
+        }
+      }).catch((error) => {
+        this.$message({
+          message: error,
+          type: 'error',
+        })
+      }).finally(() => {
+        this.loading.subt = false;
+      })
+    },
+    approveItem(index) {
+      let item = this.itemList[index]
+      if (item.membershipId) {
+        this.loading.index = index;
+        this.loading.action = true;
+        api.approveUserMembership(item.membershipId).then(res => {
+          this.itemList[index].membershipStatus = "Pass";
+        }).catch((error) => {
+          this.$message({
+            message: error,
+            type: 'error',
+          })
+        }).finally(() => {
+          this.loading.action = false;
+        })
+      }
+    },
+    rejectItem(index) {
+      let item = this.itemList[index]
+      if (item.membershipId) {
+        this.loading.index = index;
+        this.loading.action = true;
+        api.rejectUserMembership(item.membershipId).then(res => {
+          this.itemList[index].membershipStatus = "Reject";
+        }).catch((error) => {
+          this.$message({
+            message: error,
+            type: 'error',
+          })
+        }).finally(() => {
+          this.loading.action = false;
+        })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .user-sub-item-table >>> .el-table__header th {
+    background: transparent;
+    border: none;
+    padding: 10px 0;
+  }
+  .user-sub-item-table >>> td {
+    border: none;
+    padding: 1px 0;
+    background: #fff !important;
+  }
+  .user-sub-item-table >>> .cell {
+    padding: 0 8px;
+  }
+  .user-sub-item-list {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+  }
+  .item-input {
+    min-width: 80px;
+  }
+  .list-item-icon {
+    width: 30px;
+    height: 30px;
+    cursor: pointer;
+    margin: 5px 0px;
+  }
+  .list-item-icon + .list-item-icon {
+    margin-left: 15px;
+  }
+  .card-uploader {
+    padding: 5px 0;
+  }
+  .uploader-image {
+    width: 120px;
+    height: 80px;
+    text-align: left;
+  }
+  .uploader-image >>> img {
+    object-fit: cover;
+  }
+  .avatar-uploader-icon {
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    cursor: pointer;
+    font-size: 28px;
+    color: #8c939d;
+    width: 80px;
+    height: 80px;
+    line-height: 80px;
+    text-align: center;
+  }
+  .action-approve {
+    color: #82CF08;
+    cursor: pointer;
+    font-size: 12px;
+    padding: 2px 10px;
+    line-height: 15px;
+  }
+  .action-reject {
+    color: #ED3F3F;
+    cursor: pointer;
+    font-size: 12px;
+    padding: 2px 10px;
+    line-height: 15px;
+  }
+  .action-approve:hover,
+  .action-reject:hover {
+    text-decoration: underline;
+  }
+</style>

+ 189 - 0
Strides-Admin/src/views/user/views/ListVehicle.vue

@@ -0,0 +1,189 @@
+<template>
+  <el-table
+    :data="itemList"
+    class="user-sub-item-table no-border">
+    <el-table-column
+      label="Brand:"
+      min-width="90px">
+      <template slot-scope="{row}">
+        <el-input
+          class="item-input"
+          v-model="row.brand" />
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="Model:"
+      min-width="90px">
+      <template slot-scope="{row}">
+        <el-input
+          class="item-input"
+          v-model="row.model" />
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="License Plate:"
+      min-width="120px">
+      <template slot-scope="{row}">
+        <el-input
+          class="item-input"
+          v-model="row.licensePlate" />
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="Action:"
+      min-width="100px">
+      <template slot-scope="{$index,row}">
+        <div>
+          <img
+            class="list-item-icon"
+            @click="onSubtItem($index)"
+            src="../../../assets/form-list-sub.png"/>
+          <img
+            v-if="$index === itemList.length - 1"
+            class="list-item-icon"
+            @click="onAddItem"
+            src="../../../assets/form-list-add.png"/>
+        </div>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+import api from '@/http/api/apiUser'
+export default {
+  name: "ListVehicle",
+  props: {
+    list: {
+      type: Array,
+      default: () => []
+    }
+  },
+  model: {
+    prop: 'list',
+    event: 'change'
+  },
+  data() {
+    return {
+      loading: false,
+      itemList: [{
+        vehiclePk: "",
+        brand: "",
+        model: "",
+        licensePlate: "",
+        connectorType: ""
+      }],
+      rules: {
+        brand: [{
+          required: true,
+          trigger: 'blur',
+          message: 'Brand is required',
+        }],
+        model: [{
+          required: true,
+          trigger: 'blur',
+          message: 'Model is required',
+        }],
+        licensePlate: [{
+          required: true,
+          trigger: 'blur',
+          message: 'License Plate is required',
+        }]
+      }
+    };
+  },
+  watch: {
+    list: {
+      deep: true,
+      handler(n, o) {
+        this.itemList = n || [];
+        if (this.itemList.length === 0) {
+          this.onAddItem();
+        } else {
+          this.onChange();
+        }
+      }
+    }
+  },
+  methods: {
+    onChange() {
+      this.$emit('change', this.itemList);
+    },
+    onAddItem() {
+      this.itemList.push({
+        brand: '',
+        model: '',
+        licensePlate: '',
+        connectorType: ''
+      })
+      this.onChange();
+    },
+    onSubtItem(index) {
+      let item = this.itemList[index]
+      if (item.vehiclePk) {
+        this.$confirm("Are you sure you want to delete this vehicle ?", "Delete", {
+          confirmButtonText: 'OK',
+          cancelButtonText: 'Cancel',
+          type: 'warning'
+        }).then(() => {
+          this.loading = true
+          this.deleteItem(item.vehiclePk, index);
+        })
+      } else {
+        this.itemList.splice(index, 1)
+        if (this.itemList.length === 0) {
+          this.onAddItem();
+        }
+      }
+    },
+    deleteItem(id, index) {
+      api.deleteUserVehicle(id).then(() => {
+        this.itemList.splice(index, 1);
+        if (this.itemList.length === 0) {
+          this.onAddItem();
+        }
+      }).catch((error) => {
+        this.$message({
+          message: error,
+          type: 'error',
+        })
+      }).finally(() => {
+        this.loading = false
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .user-sub-item-table >>> .el-table__header th {
+    background: transparent;
+    border: none;
+    padding: 10px 0;
+  }
+  .user-sub-item-table >>> td {
+    border: none;
+    padding: 1px 0;
+    background: #fff !important;
+  }
+  .user-sub-item-table >>> .cell {
+    padding: 0 8px;
+  }
+  .user-sub-item-list {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+  }
+  .item-input {
+    min-width: 80px;
+  }
+  .list-item-icon {
+    width: 30px;
+    height: 30px;
+    cursor: pointer;
+    margin: 5px 0px;
+  }
+  .list-item-icon + .list-item-icon {
+    margin-left: 15px;
+  }
+</style>

+ 11 - 3
Strides-Admin/src/views/user/TopUp.vue → Strides-Admin/src/views/user/views/TopUp.vue

@@ -10,12 +10,19 @@
       :model="this"
       :rules="rules"
       label-width="140px"
-      label-position="left">
+      label-position="right">
+      <el-form-item
+        label="Current User:">
+        <el-input
+          class="input-text"
+          :value="userInfo.email"
+          readonly/>
+      </el-form-item>
       <el-form-item
         label="Current Amount:">
         <el-input
           class="input-text"
-          :value="userInfo.currencySymbol + ' ' + userInfo.credit"
+          :value="userInfo.credit"
           readonly/>
       </el-form-item>
       <el-form-item
@@ -45,7 +52,7 @@
 </template>
 
 <script>
-import api from '@/http/api/userManagement'
+import api from '@/http/api/apiUser'
 export default {
   name: "TopUp",
   props: {
@@ -80,6 +87,7 @@ export default {
   },
   methods: {
     hideDialog() {
+      this.amount = ""
       this.$emit("hide");
     },
     clickTopUp() {

+ 0 - 0
Strides-Admin/src/views/user/UserManagement.vue → Strides-Admin/src/views/user/views/UserManagement.vue