vbea 2 лет назад
Родитель
Сommit
06c0c74ee1

+ 3 - 2
Strides-Admin/src/components/Breadcrumb/index.vue

@@ -93,7 +93,8 @@ export default {
 
   .no-redirect {
     color: #333333;
-    cursor: text;
+    cursor: default;
+    user-select: none;
   }
   .parent-router {  
     display: inline;
@@ -103,7 +104,7 @@ export default {
       height: 30px;
     }
     a:hover {
-      color: #001489;
+      color: #001bb5;
       cursor: pointer;
     }
   }

+ 18 - 0
Strides-Admin/src/http/api/group.js

@@ -57,6 +57,24 @@ const group = {
   },
   unassignSite2Group(data) {
     return post('group/unAssignGroupSites', data)
+  },
+  getChargeTypes() {
+    return get("group/charger-types")
+  },
+  addDiscountItem(data) {
+    return post("group/discounts", data)
+  },
+  deleteDiscountItem(userGroupDiscountId) {
+    return del("group/discounts/" + userGroupDiscountId)
+  },
+  getAssignDiscountSitesPages(data) {
+    return post('group/assign-group-discount-site-pages', data)
+  },
+  assignSite2GroupDiscount(data) {
+    return post('group/assign-group-discount-sites', data)
+  },
+  unassignSite2GroupDiscount(data) {
+    return post('group/unassign-group-discount-sites', data)
   }
 }
 

+ 30 - 0
Strides-Admin/src/http/api/site.js

@@ -18,6 +18,9 @@ const site = {
   getSiteTypeList: () => {
     return get(`${MODULE_NAME}/getSiteTypeList`)
   },
+  getSiteTypeListV2: () => {
+    return get("v2/site/types")
+  },
   addSite: (data) => {
     return post(`${MODULE_NAME}/addSite`, data)
   },
@@ -78,6 +81,9 @@ const site = {
   deleteSiteLabel(siteLabelId) {
     return del("label/labels/" + siteLabelId)
   },
+  /**
+   * 分配站点标签
+   */
   getLabelAssignPages(params) {
     return post("label/assign-site-pages", params)
   },
@@ -89,6 +95,30 @@ const site = {
   },
   unassignSiteLabel(params) {
     return post("label/un-assign-sites", params)
+  },
+  /**
+   * 分配站点白名单用户
+   */
+  getPrivateUserAssignPages(params) {
+    return post("v2/site/assign-user-pages", params)
+  },
+  assignPrivateUser(params) {
+    return post("v2/site/assign-site-users", params)
+  },
+  unassignPrivateUser(params) {
+    return post("v2/site/unassign-site-users", params)
+  },
+  /**
+   * 分配站点白名单组织
+   */
+  getPrivateGroupAssignPages(params) {
+    return post("v2/site/assign-group-pages", params)
+  },
+  assignPrivateGroup(params) {
+    return post("v2/site/assign-site-groups", params)
+  },
+  unassignPrivateGroup(params) {
+    return post("v2/site/unassign-site-groups", params)
   }
 }
 

+ 11 - 3
Strides-Admin/src/router/EnergyRouter.js

@@ -2,7 +2,7 @@ import Layout from '@/layout'
 
 export default {
   path: '/smart-energy-management',
-  redirect: '/smart-energy-management/charging-profiles',
+  redirect: 'noRedirect',
   component: Layout,
   alwaysShow: true,
   meta: {
@@ -51,7 +51,11 @@ export default {
       name: 'add-charging-profile',
       meta: {
         title: 'Add Charging Profile',
-        activeMenu: '/smart-energy-management/charging-profiles'
+        activeMenu: '/smart-energy-management/charging-profiles',
+        parent: {
+          title: "Charging Profiles",
+          path: '/smart-energy-management/charging-profiles'
+        }
       },
       hidden: true
     },
@@ -61,7 +65,11 @@ export default {
       name: 'edit-charging-profile',
       meta: {
         title: 'Edit Charging Profile',
-        activeMenu: '/smart-energy-management/charging-profiles'
+        activeMenu: '/smart-energy-management/charging-profiles',
+        parent: {
+          title: "Charging Profiles",
+          path: '/smart-energy-management/charging-profiles'
+        }
       },
       hidden: true
     },

+ 11 - 3
Strides-Admin/src/router/PartnershipRouter.js

@@ -3,7 +3,7 @@ import Layout from '@/layout'
 export default {
   path: '/partnership-management',
   component: Layout,
-  redirect: '/partnership-management/service-provider-management',
+  redirect: 'noRedirect',
   alwaysShow: true,
   meta: {
     title: 'Partnership Management',
@@ -46,7 +46,11 @@ export default {
       name: 'add-service-provider',
       meta: {
         title: 'Add New Provider',
-        activeMenu: '/partnership-management/service-provider-management'
+        activeMenu: '/partnership-management/service-provider-management',
+        parent: {
+          title: 'Service Provider',
+          path: "/partnership-management/service-provider-management"
+        }
       },
       hidden: true
     },
@@ -56,7 +60,11 @@ export default {
       name: 'edit-service-provider',
       meta: {
         title: 'Edit Provider',
-        activeMenu: '/partnership-management/service-provider-management'
+        activeMenu: '/partnership-management/service-provider-management',
+        parent: {
+          title: 'Service Provider',
+          path: "/partnership-management/service-provider-management"
+        }
       },
       hidden: true
     },

+ 54 - 3
Strides-Admin/src/router/SiteRouter.js

@@ -3,7 +3,7 @@ import Layout from '@/layout'
 export default {
   path: '/site-management',
   component: Layout,
-  redirect: '/site-management/site-configuration',
+  redirect: 'noRedirect',
   meta: {
     title: 'Site Management',
     icon: 'site-management',
@@ -21,6 +21,17 @@ export default {
         activeIcon: 'sidebar-submenu-item-active',
       }
     },
+    {
+      path: '/site-management-v2/site-configuration',
+      component: () => import('@/views/site2/index'),
+      name: 'site-configuration-v2',
+      meta: {
+        breadcrumb: true,
+        title: 'Site Configuration',
+        icon: 'sidebar-submenu-item',
+        activeIcon: 'sidebar-submenu-item-active'
+      }
+    },
     {
       path: '/site-management/label-management',
       component: () => import('@/views/site-label/index'),
@@ -59,24 +70,63 @@ export default {
     {
       path: '/site-management/edit/:id',
       component: () => import('@/views/site/detail'),
+      name: "update-site-v1",
       hidden: true,
       meta: {
         title: 'Edit Site',
-        activeMenu: '/site-management/site-configuration'
+        activeMenu: '/site-management/site-configuration',
+        parent: {
+          title: "Site Configuration",
+          path: "/site-management/site-configuration"
+        }
+      }
+    },
+    {
+      path: '/site-management-v2/edit/:id',
+      component: () => import('@/views/site2/detail'),
+      name: "update-site-v2",
+      hidden: true,
+      meta: {
+        title: 'Edit Site',
+        activeMenu: '/site-management-v2/site-configuration',
+        parent: {
+          title: "Site Configuration",
+          path: "/site-management-v2/site-configuration"
+        }
       },
     },
     {
       path: '/site-management/add',
       component: () => import('@/views/site/detail'),
+      name: "add-site-v1",
       hidden: true,
       meta: {
         title: 'Add Site',
-        activeMenu: '/site-management/site-configuration'
+        activeMenu: '/site-management/site-configuration',
+        parent: {
+          title: "Site Configuration",
+          path: "/site-management/site-configuration"
+        }
+      }
+    },
+    {
+      path: '/site-management-v2/add',
+      component: () => import('@/views/site2/detail'),
+      name: "add-site-v2",
+      hidden: true,
+      meta: {
+        title: 'Add Site',
+        activeMenu: '/site-management-v2/site-configuration',
+        parent: {
+          title: "Site Configuration",
+          path: "/site-management-v2/site-configuration"
+        }
       }
     },
     {
       path: '/site-management/stations/:id',
       component: () => import('@/views/charge/RegisteredChargeStations'),
+      name: "manage-chargers",
       hidden: true,
       meta: {
         title: 'Manage Chargers',
@@ -86,6 +136,7 @@ export default {
     {
       path: '/site-management/connectors/:id',
       component: () => import('@/views/charge/Connectors'),
+      name: "manage-connectors",
       hidden: true,
       meta: {
         title: 'Manage Connectors',

+ 8 - 1
Strides-Admin/src/styles/index.scss

@@ -22,7 +22,10 @@ body {
 label {
   font-weight: 700;
 }
-
+::selection {
+  color: #FFFFFF;
+  background-color: rgba($--color-primary, 0.8);
+}
 ::-webkit-scrollbar-track-piece {
   //滚动条凹槽的颜色,还可以设置边框属性
   background-color:#f8f8f8; 
@@ -338,6 +341,10 @@ aside {
   flex: 3;
 }
 
+.bold {
+  font-weight: bold;
+}
+
 .el-message-box {
   width: 100%;
   max-width: 420px;

+ 315 - 0
Strides-Admin/src/views/company/AssignmentSite.vue

@@ -0,0 +1,315 @@
+<template>
+  <el-dialog
+    :title="title"
+    :visible="visible"
+    :before-close="onHide"
+    class="assign-label-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="assign-label-actions">
+      <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="140">
+          <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="150"/>
+        <el-table-column
+          align="center"
+          label="Select"
+          type="selection"
+          width="50"/>
+      </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 api from '@/http/api/group'
+import Pagination from '@/components/Pagination'
+export default {
+  name: "AssignmentSiteDiscount",
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    item: {
+      type: Object,
+      default: () => ({})
+    },
+    countryCode: String
+  },
+  data() {
+    return {
+      filter: {
+        pageSize: 10,
+        pageNo: 1,
+        pageVo: {
+          criteria: "",
+          countryCode: "",
+          userGroupDiscountId: "",
+          assignmentStatus: ""
+        }
+      },
+      table: {
+        data: [],
+        total: 0,
+        loading: false
+      },
+      loading: {
+        assign: false,
+        unassign: false
+      },
+      statusOptions: [],
+      selectRow: []
+    };
+  },
+  components: {Pagination},
+  computed: {
+    title() {
+      return "ASSIGN SITES (GROUP DISCOUNT)"
+    }
+  },
+  watch: {
+    visible: {
+      handler(n, o) {
+        console.log("watch.visible", n, this.item);
+        if (n) {
+          this.filter.pageVo.countryCode = this.countryCode;
+          this.filter.pageVo.userGroupDiscountId = this.item.userGroupDiscountId;
+          this.filter.pageVo.assignmentStatus = "";
+          this.onSearch()
+        }
+      }
+    }
+  },
+  mounted() {
+    this.getStatusOptions();
+  },
+  methods: {
+    onHide() {
+      this.$emit("hide", true);
+    },
+    onSearch() {
+      this.filter.pageNo = 1;
+      this.getTableData();
+    },
+    getStatusOptions() {
+      api.getAssignStatusOptions().then(res => {
+        if (res.data) {
+          this.statusOptions = res.data
+        }
+      }).catch(error => {
+        this.$message({
+          type: 'error',
+          message: error
+        })
+      })
+    },
+    getTableData() {
+      api.getAssignDiscountSitesPages(this.filter).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() {
+      const params = {
+        userGroupDiscountId: this.item.userGroupDiscountId,
+        sitePks: this.getSelectIds()
+      }
+      this.loading.assign = true;
+      api.assignSite2GroupDiscount(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;
+      })
+    },
+    onClickUnassign() {
+      const params = {
+        userGroupDiscountId: this.item.userGroupDiscountId,
+        sitePks: this.getSelectIds()
+      }
+      this.loading.unassign = true;
+      api.unassignSite2GroupDiscount(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 lang="scss" scoped>
+.assign-label-dialog
+  ::v-deep .el-dialog {
+    width: 65vw;
+    height: 90vh;
+    display: flex;
+    max-width: 1200px;
+    flex-direction: column;
+    margin-top: 5vh !important;
+  .el-dialog__header {
+    padding: 20px 20px 0;
+    font-weight: bold;
+  }
+  .el-dialog__body {
+    flex: 1;
+    padding: 20px;
+    display: flex;
+    overflow: hidden;
+    flex-direction: column;
+  }
+  @media screen and (max-width: 1200px) {
+    & {
+      width: 70vw;
+    }
+  }
+  @media screen and (max-width: 1000px) {
+    & {
+      width: 80vw;
+    }
+  }
+  @media screen and (max-width: 800px) {
+    & {
+      width: 90vw;
+    }
+  }
+  @media screen and (max-width: 700px) {
+    & {
+      width: 99vw;
+    }
+  }
+  @media screen and (max-width: 320px) {
+    & {
+      width: 100%;
+      min-width: 300px;
+    }
+  }
+}
+.assign-label-dialog .table-view {
+  flex: 1;
+  overflow-y: auto;
+  padding-top: 10px;
+  margin-bottom: -10px;
+}
+.assign-label-actions {
+  display: flex;
+  padding-top: 5px;
+  flex-wrap: wrap-reverse;
+  align-items: center;
+  justify-content: flex-end;
+}
+</style>

+ 137 - 118
Strides-Admin/src/views/company/detail.vue

@@ -5,129 +5,145 @@
       :rules="rules"
       ref="form"
       label-position="right"
-      label-width="180px">
-      <div class="content">
-        <div class="section-title">Group</div>
-        <el-form-item
-          prop="groupName"
-          label="Group Name:">
-          <el-input
-            v-model="form.groupName"
-            class="add-text"
-            placeholder=""
-            maxlength="100"/>
-        </el-form-item>
-        <el-form-item
-          prop="groupType"
-          label="Group Type:">
-          <el-select
-            v-model="form.groupType"
-            class="add-text"
-            placeholder="">
-            <el-option
-              v-for="item in options.groupType"
-              :key="item.value"
-              :label="item.name"
-              :value="item.value">
-            </el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item
-          prop="contactPerson"
-          label="Contact Person:">
-          <el-input
-            class="add-text"
-            v-model="form.contactPerson"
-            maxlength="80"/>
-        </el-form-item>
-        <el-form-item
-          prop="contactNumber"
-          label="Contact Number:">
-          <div class="add-text flexc">
+      label-width="140px">
+      <div class="flexr">
+        <div class="content flex1">
+          <div class="section-title">Group</div>
+          <el-form-item
+            prop="groupName"
+            label="Group Name:">
+            <el-input
+              v-model="form.groupName"
+              class="add-text"
+              placeholder=""
+              maxlength="100"/>
+          </el-form-item>
+          <el-form-item
+            prop="groupType"
+            label="Group Type:">
             <el-select
-              style="min-width: 75px; max-width: 80px;"
-              v-model="form.callingCode">
+              v-model="form.groupType"
+              class="add-text"
+              placeholder="">
               <el-option
-                v-for="item in options.callingCode"
-                :key="item.callingCode"
-                :label="'+' + item.callingCode"
-                :value="item.callingCode"
-              />
+                v-for="item in options.groupType"
+                :key="item.value"
+                :label="item.name"
+                :value="item.value">
+              </el-option>
             </el-select>
+          </el-form-item>
+          <el-form-item
+            prop="contactPerson"
+            label="Contact Person:">
             <el-input
-              v-model="form.contactNumber"
-              style="margin-left: 10px;"
+              class="add-text"
+              v-model="form.contactPerson"
+              maxlength="80"/>
+          </el-form-item>
+          <el-form-item
+            prop="contactNumber"
+            label="Contact Number:">
+            <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
+                v-model="form.contactNumber"
+                style="margin-left: 10px;"
+                placeholder=""
+                maxlength="20"/>
+            </div>
+          </el-form-item>
+          <el-form-item
+            prop="email"
+            label="Email Address:">
+            <el-input
+              class="add-text"
+              v-model="form.email"
+              maxlength="80"/>
+          </el-form-item>
+          <!--el-form-item
+            label="Login ID:">
+            <el-input
+              v-model="form.loginId"
+              class="add-text"
               placeholder=""
+              maxlength="50"/>
+          </el-form-item>
+          <el-form-item
+            label="Password Set:">
+            <el-input
+              v-model="form.password"
+              class="add-text"
+              type="password"
               maxlength="20"/>
+          </el-form-item-->
+          <el-form-item
+            label="Country:">
+            <el-select
+              v-model="form.countryCode"
+              class="add-text"
+              placeholder="">
+              <el-option
+                v-for="item in options.countryOptions"
+                :key="item.value"
+                :label="item.name"
+                :value="item.value">
+              </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>
+          <el-form-item
+            label="Group Logo:">
+            <el-upload
+              class="logo-upload"
+              action
+              :limit="1"
+              :show-file-list="false"
+              :file-list="[]"
+              :on-remove="file => removeLogo(file)"
+              :http-request="file => uploadLogo(file)"
+              accept=".jpg,.jpeg,.png,.gif,.JPG,.JPEG"
+              v-loading="uploading">
+              <el-image
+                v-if="logos.length > 0"
+                :src="logos[0].url"
+                title="Click to update logo"/>
+              <i v-else
+                class="el-icon-plus avatar-uploader-icon"
+                title="Click to select file"/>
+            </el-upload>
+          </el-form-item>
+        </div>
+        <div class="flex2 flexl" v-if="isEdit">
+          <div class="content">
+            <div class="section-title">Quick Summary</div>
+            <Summary :info="form.quickSummary"/>
           </div>
-        </el-form-item>
-        <el-form-item
-          prop="email"
-          label="Email Address:">
-          <el-input
-            class="add-text"
-            v-model="form.email"
-            maxlength="80"/>
-        </el-form-item>
-        <!--el-form-item
-          label="Login ID:">
-          <el-input
-            v-model="form.loginId"
-            class="add-text"
-            placeholder=""
-            maxlength="50"/>
-        </el-form-item>
-        <el-form-item
-          label="Password Set:">
-          <el-input
-            v-model="form.password"
-            class="add-text"
-            type="password"
-            maxlength="20"/>
-        </el-form-item-->
-        <el-form-item
-          label="Country:">
-          <el-select
-            v-model="form.countryCode"
-            class="add-text"
-            placeholder="">
-            <el-option
-              v-for="item in options.countryOptions"
-              :key="item.value"
-              :label="item.name"
-              :value="item.value">
-            </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>
-        <el-form-item
-          label="Group Logo:">
-          <el-upload
-            class="logo-upload"
-            action
-            :limit="1"
-            :show-file-list="false"
-            :file-list="[]"
-            :on-remove="file => removeLogo(file)"
-            :http-request="file => uploadLogo(file)"
-            accept=".jpg,.jpeg,.png,.gif,.JPG,.JPEG"
-            v-loading="uploading">
-            <el-image
-              v-if="logos.length > 0"
-              :src="logos[0].url"
-              title="Click to update logo"/>
-            <i v-else
-              class="el-icon-plus avatar-uploader-icon"
-              title="Click to select file"/>
-          </el-upload>
-        </el-form-item>
+          <div class="content flex1">
+            <div class="section-title">Discount Assignment</div>
+            <discounts
+              :data="form.discounts"
+              :group="form.groupPk"
+              :countryCode="form.countryCode"
+              @change="getGroupDetail"/>
+          </div>
+        </div>
       </div>
       <div class="content flexcr">
         <div class="buttons">
@@ -163,6 +179,8 @@ import api from '@/http/api/group'
 import provider from '../../http/api/provider'
 import setting from '../../settings.js'
 import {getCountryList} from '../../utils/index.js'
+import Summary from './summary.vue'
+import discounts from './discounts.vue'
 export default {
   name: "GroupDetail",
   data() {
@@ -234,6 +252,7 @@ export default {
       }
     }
   },
+  components: {Summary, discounts},
   created() {
     this.getCountryList()
     this.getGroupType()
@@ -381,13 +400,13 @@ export default {
   @import '../../styles/variables.scss';
   .container {
     width: 100%;
-    padding: 20px 60px;
+    padding: 20px 40px;
     min-height: $mainAppMinHeight;
     background-color: #F0F5FC;
   }
   .content {
     margin: 0 8px 16px;
-    padding: 15px 80px;
+    padding: 15px 50px;
     border-radius: 6px;
     background-color: white;
   }

+ 253 - 0
Strides-Admin/src/views/company/discounts.vue

@@ -0,0 +1,253 @@
+<template>
+  <div>
+    <el-table
+      :data="tableData"
+      class="discount-table"
+      header-row-class-name="customer-row"
+      row-class-name="customer-row"
+      v-loading="loading">
+      <el-table-column
+        label="Charge Type:"
+        min-width="120px">
+        <template slot-scope="{row}">
+          <el-select
+            v-model="row.chargeType"
+            class="add-text"
+            placeholder="">
+            <el-option
+              v-for="item in typeOptions"
+              :key="item"
+              :label="item"
+              :value="item">
+            </el-option>
+          </el-select>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="Eligible Sites:"
+        min-width="120px"
+        align="center">
+        <template slot-scope="{row}">
+          <span
+            class="link-detail underline bold"
+            @click="showAssignment(row)">{{row.discountSiteLable}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="Discount(%):"
+        min-width="100px">
+        <template slot-scope="{row}">
+          <el-input
+            v-model="row.discount"
+            class="add-text"
+            min="0"
+            max="100"
+            type="number"
+            maxlength="5"/>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label=""
+        width="100px">
+        <template slot-scope="{row, $index}">
+          <div class="flexc">
+            <img
+              class="list-item-icon"
+              @click="subtItem(row, $index)"
+              src="../../assets/form-list-sub.png"/>
+            <img
+              v-if="tableData.length - 1 === $index"
+              class="list-item-icon"
+              @click="addItem"
+              src="../../assets/form-list-add.png"/>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+    <AssignmentSite
+      :countryCode="countryCode"
+      :visible="assignment.visible"
+      :item="assignment.item"
+      @hide="hideAssignment"/>
+  </div>
+</template>
+
+<script>
+import api from '@/http/api/group';
+import AssignmentSite from './AssignmentSite.vue';
+export default {
+  name: "DiscountAssignment",
+  props: {
+    data: {
+      type: Array,
+      default: () => []
+    },
+    group: {
+      type: String | Number,
+      default: ""
+    },
+    countryCode: String
+  },
+  data() {
+    return {
+      loading: false,
+      typeOptions: [],
+      tableData: [],
+      assignment: {
+        visible: false,
+        item: {}
+      }
+    };
+  },
+  components: {
+    AssignmentSite
+  },
+  mounted() {
+    this.getTypeOptions();
+  },
+  watch: {
+    group: {
+      handler(pk, old) {
+        if (pk) {
+          this.init();
+        }
+      }
+    },
+    data: {
+      deep: true,
+      handler(n, o) {
+        this.$nextTick(() => {
+          if (this.group) {
+            this.init();
+            console.log("监听变化");
+          }
+        });
+      }
+    }
+  },
+  methods: {
+    init() {
+      this.tableData = this.data;
+      if (this.tableData) {
+        if (this.tableData.length == 0) {
+          this.addItem();
+        }
+      } else {
+        this.tableData = [];
+        this.addItem();
+      }
+    },
+    onChange() {
+      this.$emit("change")
+    },
+    getTypeOptions() {
+      api.getChargeTypes().then(res => {
+        if (res.data) {
+          this.typeOptions = res.data
+        }
+      }).catch(err => {
+        this.$message({
+          message: err,
+          type: 'error'
+        })
+      })
+    },
+    validDiscount() {
+      let pattern = /^\d*(\.)?\d{1,2}$/
+      if (pattern.test(value)) {
+        const lang = Number(value);
+        if (lang < 0 || lang > 100) {
+          return "Discount must to be between 0 and 100";
+        } else {
+          return "";
+        }
+      } else {
+        return "Please enter the correct discount";
+      }
+    },
+    showAssignment(row) {
+      this.assignment.item = row;
+      this.assignment.visible = true;
+    },
+    hideAssignment() {
+      this.assignment.item = {};
+      this.assignment.visible = false;
+      this.onChange();
+    },
+    addItem() {
+      this.loading = true;
+      api.addDiscountItem({
+        groupPk: this.group
+      }).then(res => {
+        this.loading = false;
+        if (res.data) {
+          this.tableData.push(res.data)
+        }
+      }).catch(err => {
+        this.loading = false;
+        this.$message({
+          message: err,
+          type: 'error'
+        })
+      })
+    },
+    subtItem(item, index) {
+      this.$confirm('Confirm delete?', 'Delete', {
+        confirmButtonText: 'Confirm',
+        cancelButtonText: 'Cancel',
+        type: 'warning',
+      }).then(() => {
+        this.deleteDiscount(item.userGroupDiscountId, index)
+      })
+    },
+    deleteDiscount(id, index) {
+      if (id) {
+        this.loading = true;
+        api.deleteDiscountItem(id).then(res => {
+          this.loading = false;
+          this.tableData.splice(index, 1);
+        }).catch(err => {
+          this.loading = false;
+          this.$message({
+            message: err,
+            type: 'error'
+          })
+        })
+      } else {
+        this.tableData.splice(index, 1);
+      }
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+  .discount-table {
+    max-width: 500px;
+    padding-bottom: 10px;
+  }
+  .discount-table:before {
+    height: 0;
+  }
+  .add-text {
+    width: 100%;
+  }
+  ::v-deep .customer-row {
+    th,td {
+      background: #fff;
+      border-bottom: none;
+    }
+    .time-picker {
+      width: 100%;
+    }
+  }
+  .list-item-icon {
+    height: 30px;
+    cursor: pointer;
+    font-size: 26px;
+    font-weight: 500;
+  }
+  .list-item-icon + .list-item-icon {
+    margin-left: 15px;
+  }
+</style>

+ 117 - 0
Strides-Admin/src/views/company/summary.vue

@@ -0,0 +1,117 @@
+<template>
+  <div class="flexcr dashboard">
+    <div class="dashboard-item" v-for="(item, index) in dashboard" :key="index">
+      <div class="item-value" v-if="info">{{info[item.key] || "0"}}</div>
+      <div class="item-title">{{item.title}}</div>
+      <el-button
+        type="primary"
+        class="item-button"
+        :disabled="!enable"
+        @click="item.onClick">
+        {{item.button}}
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: "GroupSummary",
+    props: {
+      siteId: String|Number,
+      info: {
+        type: Object,
+        default: () => {}
+      },
+      enable: {
+        type: Boolean,
+        default: true
+      }
+    },
+    data() {
+      return {
+        siteInfo: {},
+        dashboard: [{
+          key: "creditLimit",
+          title: "CREDIT LIMIT",
+          button: "CONFIGURE LIMIT",
+          onClick: () => this.toCreditLimit()
+        },{
+          key: "allowedSites",
+          title: "ALLOWED SITES",
+          button: "CONFIGURE SITES",
+          onClick: () => this.toSiteMgmt()
+        },{
+          key: "users",
+          title: "USERS",
+          button: "VIEW USERS",
+          onClick: () => this.toUsers()
+        },{
+          key: "transactions",
+          title: "TRANSACTIONS",  
+          button: "VIEW TRANSACTIONS",
+          onClick: () => this.toTransactions()
+        }]
+      };
+    },
+    mounted() {
+      
+    },
+    methods: {
+      toCreditLimit() {
+        this.$router.push({
+          path: "/partnership-management/monthly-credit-management"
+        })
+      },
+      toSiteMgmt() {
+        this.$router.push({
+          path: "/partnership-management/group-management"
+        })
+      },
+      toTransactions() {
+        this.$router.push({
+          path: "/station-activities/transactions"
+        })
+      },
+      toUsers() {
+        this.$router.push({
+          path: "/user-management"
+        })
+      }
+    }
+  }
+</script>
+
+<style scoped>
+.dashboard {
+  zoom: 0.88;
+}
+.dashboard-item {
+  flex: 1;
+  display: flex;
+  margin: 5px 8px;
+  align-items: center;
+  flex-direction: column;
+}
+.item-value {
+  color: #333;
+  font-size: 22px;
+  font-weight: bold;
+  line-height: 30px;
+}
+.item-title {
+  color: #333;
+  font-size: 14px;
+  line-height: 20px;
+  white-space: nowrap;
+  text-transform: uppercase;
+}
+.item-button {
+  width: 100%;
+  max-width: 200px;
+  font-size: 13px;
+  margin-top: 10px;
+  padding: 12px 10px;
+  text-transform: uppercase;
+}
+</style>

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

@@ -94,7 +94,7 @@
       <el-table-column
         label="Action"
         align="center"
-        min-width="140">
+        width="80">
         <template slot-scope="{row}">
           <!-- <TableAction
             :showEdit="!$route.meta.onlyView"

+ 8 - 0
Strides-Admin/src/views/settlement/BillingAccount.vue

@@ -22,6 +22,14 @@
           @change="onClickSearch"
           @keyup.enter.native="onClickSearch"
           clearable/>
+        <div class="filter-flex-button">
+          <el-button
+            type="primary"
+            icon="el-icon-plus"
+            @click="onClickAdd">
+            Create
+          </el-button>
+        </div>
       </div>
     </div>
     <el-table

+ 2 - 2
Strides-Admin/src/views/settlement/assignment.vue

@@ -102,7 +102,7 @@
             align="center"
             label="Assignment Status"
             prop="assignmentStatus"
-            min-width="130"/>
+            min-width="150"/>
         </template>
         <el-table-column
           align="center"
@@ -127,7 +127,7 @@
 import api from '../../http/api/financial.js'
 import Pagination from '@/components/Pagination'
 export default {
-  name: "AssignmentSiteLabel",
+  name: "AssignmentSiteSettle",
   props: {
     visible: {
       type: Boolean,

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

@@ -70,12 +70,12 @@
       },
       toTransactions() {
         this.$router.push({
-          path: "/transactions-reservations/transactions"
+          path: "/station-activities/transactions"
         })
       },
       toReservations() {
         this.$router.push({
-          path: "/transactions-reservations/reservations"
+          path: "/station-activities/reservations"
         })
       }
     }

+ 1 - 1
Strides-Admin/src/views/site/components/SiteTypeWithTime.vue

@@ -80,7 +80,7 @@
 <script>
 import site from '@/http/api/site'
 export default {
-  name: "",
+  name: "SiteTypeWithTimeV1",
   props: {
     siteTypes: {
       type: Array,

+ 319 - 0
Strides-Admin/src/views/site2/components/AssignmentGroup.vue

@@ -0,0 +1,319 @@
+<template>
+  <el-dialog
+    :title="title"
+    :visible="visible"
+    :before-close="onHide"
+    class="assign-group-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="assign-label-actions">
+      <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="Name"
+          prop="name"
+          min-width="120"/>
+        <el-table-column
+          align="center"
+          label="Type"
+          prop="type"
+          min-width="120"/>
+        <el-table-column
+          align="center"
+          label="Country"
+          prop="country"
+          min-width="120"/>
+        <el-table-column
+          align="center"
+          label="Approved"
+          prop="approved"
+          min-width="120"/>
+        <el-table-column
+          align="center"
+          label="Pending"
+          prop="pending"
+          min-width="120"/>
+        <el-table-column
+          align="center"
+          label="Assignment Status"
+          prop="assignmentStatus"
+          min-width="150"
+          v-if="false"/>
+        <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 api from '@/http/api/site.js'
+import Pagination from '@/components/Pagination'
+export default {
+  name: "AssignmentPrivateGroup",
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    id: {
+      type: String | Number,
+      default: ""
+    }
+  },
+  data() {
+    return {
+      filter: {
+        pageSize: 10,
+        pageNo: 1,
+        pageVo: {
+          sitePk: "",
+          criteria: "",
+          assignmentStatus: ""
+        }
+      },
+      table: {
+        data: [],
+        total: 0,
+        loading: false
+      },
+      loading: {
+        assign: false,
+        unassign: false
+      },
+      statusOptions: [],
+      selectRow: []
+    };
+  },
+  components: {Pagination},
+  computed: {
+    title() {
+      return "ASSIGN GROUP (SITE WHITELIST)" 
+    }
+  },
+  watch: {
+    visible: {
+      handler(n, o) {
+        console.log("watch.visible", n, o);
+        if (n) {
+          this.filter.pageVo.sitePk = this.id;
+          this.filter.pageVo.assignmentStatus = "";
+          this.onSearch()
+        }
+      }
+    }
+  },
+  mounted() {
+    this.getStatusOptions();
+  },
+  methods: {
+    onHide() {
+      this.$emit("hide", true);
+    },
+    onSearch() {
+      this.filter.pageNo = 1;
+      this.getTableData();
+    },
+    getStatusOptions() {
+      api.getAssignStatusOptions().then(res => {
+        if (res.data) {
+          this.statusOptions = res.data
+        }
+      }).catch(error => {
+        this.$message({
+          type: 'error',
+          message: error
+        })
+      })
+    },
+    getTableData() {
+      api.getPrivateGroupAssignPages(this.filter).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.groupPk)
+      })
+      return ids;
+    },
+    onClickAssign() {
+      const params = {
+        sitePk: this.id,
+        groupPks: this.getSelectIds()
+      }
+      this.loading.assign = true;
+      api.assignPrivateGroup(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;
+      })
+    },
+    onClickUnassign() {
+      const params = {
+        sitePk: this.id,
+        groupPks: this.getSelectIds()
+      }
+      this.loading.unassign = true;
+      api.unassignPrivateGroup(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 lang="scss" scoped>
+.assign-group-dialog
+  ::v-deep .el-dialog {
+    width: 65vw;
+    height: 90vh;
+    display: flex;
+    max-width: 1200px;
+    flex-direction: column;
+    margin-top: 5vh !important;
+  .el-dialog__header {
+    padding: 20px 20px 0;
+    font-weight: bold;
+  }
+  .el-dialog__body {
+    flex: 1;
+    padding: 20px;
+    display: flex;
+    overflow: hidden;
+    flex-direction: column;
+  }
+  @media screen and (max-width: 1200px) {
+    & {
+      width: 70vw;
+    }
+  }
+  @media screen and (max-width: 1000px) {
+    & {
+      width: 80vw;
+    }
+  }
+  @media screen and (max-width: 800px) {
+    & {
+      width: 90vw;
+    }
+  }
+  @media screen and (max-width: 700px) {
+    & {
+      width: 99vw;
+    }
+  }
+  @media screen and (max-width: 320px) {
+    & {
+      width: 100%;
+      min-width: 300px;
+    }
+  }
+}
+.assign-group-dialog .table-view {
+  flex: 1;
+  overflow-y: auto;
+  padding-top: 10px;
+  margin-bottom: -10px;
+}
+.assign-label-actions {
+  display: flex;
+  padding-top: 5px;
+  flex-wrap: wrap-reverse;
+  align-items: center;
+  justify-content: flex-end;
+}
+</style>

+ 316 - 0
Strides-Admin/src/views/site2/components/AssignmentUser.vue

@@ -0,0 +1,316 @@
+<template>
+  <el-dialog
+    :title="title"
+    :visible="visible"
+    :before-close="onHide"
+    class="assign-user-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="assign-label-actions">
+      <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="Name"
+          prop="name"
+          min-width="120"/>
+        <el-table-column
+          align="center"
+          label="E-mail"
+          prop="email"
+          min-width="120"/>
+        <el-table-column
+          align="center"
+          label="Vehicle"
+          min-width="140">
+          <template slot-scope="{row}">
+            <div v-for="item in row.vehicles" :key="item">{{item}}</div>
+          </template>
+        </el-table-column>
+        <el-table-column
+          align="center"
+          label="Number"
+          prop="number"
+          min-width="120"/>
+        <el-table-column
+          align="center"
+          label="Account 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 api from '@/http/api/site.js'
+import Pagination from '@/components/Pagination'
+export default {
+  name: "AssignmentPrivateUser",
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    id: {
+      type: String | Number,
+      default: ""
+    }
+  },
+  data() {
+    return {
+      filter: {
+        pageSize: 10,
+        pageNo: 1,
+        pageVo: {
+          sitePk: "",
+          criteria: "",
+          assignmentStatus: ""
+        }
+      },
+      table: {
+        data: [],
+        total: 0,
+        loading: false
+      },
+      loading: {
+        assign: false,
+        unassign: false
+      },
+      statusOptions: [],
+      selectRow: []
+    };
+  },
+  components: {Pagination},
+  computed: {
+    title() {
+      return "ASSIGN USERS (SITE WHITELIST)" 
+    }
+  },
+  watch: {
+    visible: {
+      handler(n, o) {
+        console.log("watch.visible", n, o);
+        if (n) {
+          this.filter.pageVo.sitePk = this.id;
+          this.filter.pageVo.assignmentStatus = "";
+          this.onSearch()
+        }
+      }
+    }
+  },
+  mounted() {
+    this.getStatusOptions();
+  },
+  methods: {
+    onHide() {
+      this.$emit("hide", true);
+    },
+    onSearch() {
+      this.filter.pageNo = 1;
+      this.getTableData();
+    },
+    getStatusOptions() {
+      api.getAssignStatusOptions().then(res => {
+        if (res.data) {
+          this.statusOptions = res.data
+        }
+      }).catch(error => {
+        this.$message({
+          type: 'error',
+          message: error
+        })
+      })
+    },
+    getTableData() {
+      api.getPrivateUserAssignPages(this.filter).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.userPk)
+      })
+      return ids;
+    },
+    onClickAssign() {
+      const params = {
+        sitePk: this.id,
+        userPks: this.getSelectIds()
+      }
+      this.loading.assign = true;
+      api.assignPrivateUser(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;
+      })
+    },
+    onClickUnassign() {
+      const params = {
+        sitePk: this.id,
+        userPks: this.getSelectIds()
+      }
+      this.loading.unassign = true;
+      api.unassignPrivateUser(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 lang="scss" scoped>
+.assign-user-dialog
+  ::v-deep .el-dialog {
+    width: 65vw;
+    height: 90vh;
+    display: flex;
+    max-width: 1200px;
+    flex-direction: column;
+    margin-top: 5vh !important;
+  .el-dialog__header {
+    padding: 20px 20px 0;
+    font-weight: bold;
+  }
+  .el-dialog__body {
+    flex: 1;
+    padding: 20px;
+    display: flex;
+    overflow: hidden;
+    flex-direction: column;
+  }
+  @media screen and (max-width: 1200px) {
+    & {
+      width: 70vw;
+    }
+  }
+  @media screen and (max-width: 1000px) {
+    & {
+      width: 80vw;
+    }
+  }
+  @media screen and (max-width: 800px) {
+    & {
+      width: 90vw;
+    }
+  }
+  @media screen and (max-width: 700px) {
+    & {
+      width: 99vw;
+    }
+  }
+  @media screen and (max-width: 320px) {
+    & {
+      width: 100%;
+      min-width: 300px;
+    }
+  }
+}
+.assign-user-dialog .table-view {
+  flex: 1;
+  overflow-y: auto;
+  padding-top: 10px;
+  margin-bottom: -10px;
+}
+.assign-label-actions {
+  display: flex;
+  padding-top: 5px;
+  flex-wrap: wrap-reverse;
+  align-items: center;
+  justify-content: flex-end;
+}
+</style>

+ 310 - 0
Strides-Admin/src/views/site2/components/SiteTypeWithTime.vue

@@ -0,0 +1,310 @@
+<template>
+  <div>
+    <el-table
+      :data="siteTypeTable"
+      class="type-table"
+      header-row-class-name="customer-row"
+      row-class-name="customer-row">
+      <el-table-column
+        label="Site Type With Schedule"
+        prop="label"
+        width="200px">
+        <template slot="header">
+          <div>
+            <span class="require">*</span>
+            Site Type With Schedule
+          </div>
+        </template>
+        <template slot-scope="{row}">
+          <span>{{ row.label }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="All Day"
+        width="80px">
+        <template slot="header">
+          <div>
+            <span class="require">*</span>
+            All Day
+          </div>
+        </template>
+        <template slot-scope="{row, $index}">
+          <el-switch
+            v-model="row.allDay"
+            :disabled="isSiteTypeEnable($index, true)"
+            @change="changeAllDay"/>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="Start Time"
+        min-width="120px">
+        <template slot="header">
+          <div>
+            <span class="require1">*</span>
+            Start Time
+          </div>
+        </template>
+        <template slot-scope="{row, $index}">
+          <el-time-picker
+            class="time-picker"
+            v-model="row.startTime"
+            format="HH:mm"
+            value-format="HH:mm"
+            placeholder="Start Time"
+            :disabled="isSiteTypeEnable($index)">
+          </el-time-picker>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="End Time"
+        min-width="120px">
+        <template slot="header">
+          <div>
+            <span class="require1">*</span>
+            End Time
+          </div>
+        </template>
+        <template slot-scope="{row, $index}">
+          <el-time-picker
+            class="time-picker"
+            v-model="row.endTime"
+            format="HH:mm"
+            value-format="HH:mm"
+            placeholder="End Time"
+            :disabled="isSiteTypeEnable($index)">
+          </el-time-picker>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="Eligible Public User"
+        width="152px"
+        align="center"
+        v-if="sitePk">
+        <template slot-scope="{row, $index}">
+          <span
+            class="link-detail underline bold"
+            @click="showAssignDialog(false, $index)"
+            v-if="row.userEligible">{{row.userEligible}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column width="152px" v-else></el-table-column>
+      <el-table-column
+        label="Eligible Group"
+        width="120px"
+        align="center"
+        v-if="sitePk">
+        <template slot-scope="{row, $index}">
+          <span
+            class="link-detail underline bold"
+            @click="showAssignDialog(true, $index)"
+            v-if="row.groupEligible">{{row.groupEligible}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column width="120px" v-else></el-table-column>
+    </el-table>
+    <AssignmentGroup
+      v-bind="assignGroup"
+      @hide="hideAssignDialog"/>
+    <AssignmentUser
+      v-bind="assignUser"
+      @hide="hideAssignDialog"/>
+  </div>
+</template>
+
+<script>
+import site from '@/http/api/site'
+import AssignmentGroup from './AssignmentGroup.vue';
+import AssignmentUser from './AssignmentUser.vue';
+export default {
+  name: "SiteTypeWithTimeV2",
+  props: {
+    siteTypes: {
+      type: Array,
+      default: []
+    },
+    sitePk: {
+      type: String | Number,
+      default: ""
+    }
+  },
+  components: {AssignmentUser, AssignmentGroup},
+  data() {
+    return {
+      isInit: false,
+      siteTypeTable: [{
+        siteType: "",
+        allDay: false,
+        startTime: "",
+        endTime: "",
+        userEligible: "",
+        groupEligible: ""
+      }],
+      assignUser: {
+        id: "",
+        visible: false
+      },
+      assignGroup: {
+        id: "",
+        visible: false
+      }
+    };
+  },
+  watch: {
+    siteTypes: {
+      deep: true,
+      handler(n, o) {
+        if (n && n.length) {
+          this.init();
+        }
+      }
+    },
+    siteTypeTable: {
+      deep: true,
+      handler(n, o) {
+        if (this.isInit) {
+          this.isInit = false;
+          console.log("跳过onChange");
+        } else {
+          this.onChange();
+        }
+      }
+    }
+  },
+  mounted() {
+    this.getSiteTypeOptions();
+  },
+  methods: {
+    onChange() {
+      this.$emit('change', this.siteTypeTable);
+    },
+    init() {
+      this.isInit = true;
+      const types = []
+      for (let item of this.siteTypeTable) {
+        for (let t of this.siteTypes) {
+          if (item.siteType == t.siteType) {
+            types.push({
+              label: item.label,
+              siteType: item.siteType,
+              allDay: t.allDay,
+              startTime: t.startTime,
+              endTime: t.endTime,
+              userEligible: t.userEligible,
+              groupEligible: t.groupEligible
+            })
+            break;
+          }
+        }
+      }
+      this.siteTypeTable = types;
+      if (this.sitePk) {
+        this.assignUser.id = this.sitePk;
+        this.assignGroup.id = this.sitePk;
+      }
+    },
+    changeAllDay(allDay) {
+      if (allDay) {
+        this.siteTypeTable.forEach(item => {
+          item.startTime = "";
+          item.endTime = "";
+        })
+      }
+    },
+    getSiteTypeOptions() {
+      site.getSiteTypeListV2().then(res => {
+        if (res.data && res.data.length) {
+          this.siteTypeTable = [];
+          res.data.forEach(item => {
+            this.siteTypeTable.push({
+              label: item.name,
+              siteType: item.value,
+              allDay: false,
+              startTime: "",
+              endTime: "",
+              userEligible: item.users,
+              groupEligible: item.groups
+            })
+          })
+          if (this.siteTypes && this.siteTypes.length) {
+            this.init();
+          }
+        }
+      })
+    },
+    isAnyAllDay() {
+      for (let i = 0; i < this.siteTypeTable.length; i++) {
+        let type = this.siteTypeTable[i];
+        if (type.allDay) {
+          return true;
+        } else {
+          continue;
+        }
+      }
+      return false;
+    },
+    /**判断是否应该启用组件
+     * @param {Object} index 索引
+     * @param {Object} isSwitch 是否是Switch组件
+     */
+    isSiteTypeEnable(index, isSwitch) {
+      for (let i = 0; i < this.siteTypeTable.length; i++) {
+        let type = this.siteTypeTable[i];
+        if (type.allDay) {
+          if (index == i && isSwitch) {
+            return false;
+          } else {
+            return true;
+          }
+        } else {
+          continue;
+        }
+      }
+      return false;
+    },
+    showAssignDialog(isGroup, index) {
+      let type = this.siteTypeTable[index];
+      if (type.allDay || (type.startTime && type.endTime)) {
+        if (isGroup) {
+          this.assignGroup.visible = true;
+        } else {
+          this.assignUser.visible = true;
+        }
+      }
+    },
+    hideAssignDialog() {
+      this.assignUser.visible = false;
+      this.assignGroup.visible = false;
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+  .type-table {
+    width: 100%;
+    min-width: 520px;
+    max-width: 1000px;
+    padding-bottom: 10px;
+  }
+  .type-table:before {
+    height: 0;
+  }
+  .require {
+    color: #f56c6c;
+    font-size: 14px;
+    margin-left: -8px;
+  }
+  .require1 {
+    color: #f56c6c;
+    font-size: 14px;
+  }
+  ::v-deep .customer-row {
+    th,td {
+      background: #fff;
+      border-bottom: none;
+    }
+    .time-picker {
+      width: 100%;
+    }
+  }
+</style>

+ 989 - 0
Strides-Admin/src/views/site2/detail.vue

@@ -0,0 +1,989 @@
+<template>
+  <div class="view-container" v-loading="pageLoading">
+    <el-form
+      ref="addForm"
+      :rules="rules"
+      :model="siteForm"
+      label-position="left"
+      label-width="120px"
+      style="width: 100%;">
+      <div class="flexr">
+        <div class="flexl flex1">
+          <div class="view-content flex1">
+            <div class="section-title" id="idSiteInfo">Site Information</div>
+            <el-form-item
+              label="Site Name:"
+              prop="siteName">
+              <el-input
+                :disabled="isMCSTUser"
+                v-model="siteForm.siteName"
+                class="input-text" />
+            </el-form-item>
+            <el-form-item
+              prop="locationLatitude"
+              label="Latitude:">
+              <el-input
+                :disabled="isMCSTUser"
+                v-model="siteForm.locationLatitude"
+                class="input-text" />
+            </el-form-item>
+            <el-form-item
+              prop="locationLongitude"
+              label="Longitude:">
+              <el-input
+                :disabled="isMCSTUser"
+                v-model="siteForm.locationLongitude"
+                class="input-text"/>
+            </el-form-item>
+            <el-form-item
+              prop="address.countryCode"
+              label="Country:">
+              <el-select
+                v-model="siteForm.address.countryCode"
+                class="input-text"
+                :disabled="isMCSTUser">
+                <el-option
+                  v-for="item in countryOptions"
+                  :key="item.name"
+                  :label="item.name"
+                  :value="item.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              prop="address.city"
+              label="City:">
+              <el-input
+                v-model="siteForm.address.city"
+                class="input-text"
+                :disabled="isMCSTUser"/>
+            </el-form-item>
+            <el-form-item
+              prop="address.street"
+              label="Street:">
+              <el-input
+                v-model="siteForm.address.street"
+                class="input-text"
+                :disabled="isMCSTUser"/>
+            </el-form-item>
+            <el-form-item
+              prop="address.zipCode"
+              label="Postal Code:">
+              <el-input
+                v-model="siteForm.address.zipCode"
+                class="input-text"
+                :disabled="isMCSTUser"/>
+            </el-form-item>
+          </div>
+          <div class="view-content">
+            <div class="section-title">Additional Information</div>
+            <el-form-item
+              label-width="130px"
+              label="Operating Hours:"
+              class="flex-item">
+              <div class="flexc">
+                <el-input
+                  v-model="siteForm.operatingHours"
+                  :disabled="isMCSTUser || !!siteForm.endlessService"
+                  class="input-text" />
+                <el-switch
+                  :disabled="isMCSTUser"
+                  @change="changeEndless"
+                  style="margin-left: 10px"
+                  inactive-text="24/7"
+                  v-model="siteForm.endlessService">
+                </el-switch>
+              </div>
+            </el-form-item>
+            <el-form-item
+              label-width="130px"
+              label="Parking Fee:"
+              class="flex-item">
+              <div class="flexc">
+                <el-input
+                  :disabled="isMCSTUser || !!siteForm.free"
+                  v-model="siteForm.parkingFee"
+                  class="input-text" />
+                <el-switch
+                  :disabled="isMCSTUser"
+                  @change="changeParkingFree"
+                  style="margin-left: 10px"
+                  inactive-text="Free"
+                  v-model="siteForm.free">
+                </el-switch>
+              </div>
+            </el-form-item>
+            <el-form-item
+              label-width="130px"
+              label="Additional Notes:">
+              <el-input
+                :disabled="isMCSTUser"
+                v-model="siteForm.additionalNotes"
+                type="textarea"
+                :autosize="autosize"
+                class="input-text" />
+            </el-form-item>
+            <el-form-item
+              label-width="130px"
+              label="Carpark Code:">
+              <el-input
+                v-model="siteForm.carParkCode"
+                class="input-text" />
+            </el-form-item>
+          </div>
+        </div>
+        <div class="flexl flex1">
+          <div class="view-content" style="padding: 15px;">
+            <dashboard
+              :enable="isEdit"
+              :info="summaryInfo"
+              :siteId="siteForm.sitePk"/>
+          </div>
+          
+          <div class="view-content flex1">
+            <div class="section-title flexcr">
+              Site Summary
+              <div class="section-sub-title">(Currency Used: {{currencyData[siteForm.address.countryCode]}})</div>
+            </div>
+            <Summary
+              :info="summaryInfo"
+              :currency="currencyData[siteForm.address.countryCode]"/>
+          </div>
+          
+          <div class="view-content" id="idSiteTypes">
+            <div class="section-title">Site EMAIL Configuration</div>
+            <email-recipient v-model="siteForm.emails"/>
+          </div>
+        </div>
+      </div>
+      
+      <div class="view-content">
+        <div class="section-title">TIME CONFIGURATION</div>
+        <div class="site-type-layout">
+          <site-type-with-time
+            :siteTypes="siteForm.siteTypes"
+            :sitePk="siteForm.sitePk"
+            @change="changeSiteTypes"/>
+        </div>
+      </div>
+      
+      <div class="view-content">
+        <div class="section-title">OCPP SETTINGS</div>
+        <div class="flexr">
+          <div class="flex2">
+            <el-form-item
+              label-width="150px"
+              label="Heartbeat Interval:"
+              prop="heartbeat"
+              class="flex1">
+              <div class="flexc">
+                <el-input
+                  class="value-text"
+                  v-model="siteForm.ocppSetting.heartbeatIntervalMinutes"
+                  placeholder="Add text"
+                  maxlength="5"/>
+                <span class="value-unit">Minutes</span>
+              </div>
+            </el-form-item>
+            <div class="tips">The time interval in <i>minutes</i> for how often a charge point should request the current time from the CSMS.</div>
+          </div>
+          <div class="flex1">
+            <p></p>
+          </div>
+          <div class="flex2">
+            <el-form-item
+              label="Expiration:"
+              prop="expiration"
+              label-width="95px"
+              class="flex1">
+              <div class="flexc">
+                <el-input
+                  class="value-text"
+                  v-model="siteForm.ocppSetting.expireHours"
+                  placeholder="Add text"
+                  maxlength="5"/>
+                <span class="value-unit">Hours</span>
+              </div>
+            </el-form-item>
+            <div class="tips" style="padding-left: 95px;">The amount of time in <i>hours</i> for how long a charge point should cache the authorization info of an idTag in its local white list, if an expiry date is not explicitly set. 
+            The value 0 disables this functionality 
+              (i.e.no expiry date will be set).</div>
+          </div>
+        </div>
+      </div>
+      
+      <div class="view-content" id="idChargeRates">
+        <!--div class="section-title flexcr">
+          CHARGE SITE RATE
+          <div class="section-sub-title">(Currency Used: {{currencyData[siteForm.address.countryCode]}})</div>
+        </div>
+        <charge-rate
+          v-model="ratesForm.chargeRates"/>
+        <div class="sparator"></div>
+        <div class="section-title flexcr">
+          SPECIAL CHARGE RATE
+          <div class="section-sub-title">(Currency Used: {{currencyData[siteForm.address.countryCode]}})</div>
+        </div>
+        <charge-rate
+          isSpecial
+          v-model="ratesForm.specialChargeRates"/>
+        <div class="sparator"></div>-->
+        <div class="section-title">Reservations</div>
+        <reservation
+          :enabled.sync="siteForm.enableReservation"
+          :limit.sync="siteForm.timeLimit"
+        />
+        <div class="sparator" v-if="isEdit"></div>
+        <div class="section-title" v-if="isEdit">LOAD BALANCING</div>
+        <!-- <Balancing v-model="balancingForm"/> -->
+        <div class="new-load-balance" v-if="isEdit" style="padding-bottom: 20px;">
+          <span class="new-text">NEW!</span>
+          <span class="link-type" @click="configLoadBalance">Configure load balancing in a new window</span>
+        </div>
+        <template v-if="enableWhitelistFleet">
+          <div class="sparator"></div>
+          <div class="section-title">WHITELIST GROUP</div>
+          <white-list-fleet v-model="siteForm.groupWhitelist"/>
+        </template>
+        
+        <template v-if="enableWhitelistUser">
+          <div class="sparator"></div>
+          <div class="section-title">WHITELIST USERS</div>
+          <white-list-user v-model="siteForm.whitelistUser"/>
+        </template>
+        <div class="sparator"></div>
+        <div class="section-title flexcwr">
+          Idle Fee SETTINGS
+          <div class="idle-switch">
+            <el-switch
+              inactive-text="Enable Idle Fee"
+              v-model="siteForm.enableIdleFee"/>
+          </div>
+        </div>
+        <el-row :gutter="20" v-if="siteForm.enableIdleFee">
+          <el-col :xs="24" :md="12" :lg="8">
+            <el-form-item
+              label="Effective From:"
+              label-width="130px"
+              prop="effectiveFrom">
+              <el-time-picker
+                v-model="siteForm.effectiveFrom"
+                class="input-text"
+                format="HH:mm"
+                value-format="HH:mm"/>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :md="12" :lg="8">
+            <el-form-item
+              label="Effective To:"
+              label-width="130px"
+              prop="effectiveTo">
+              <el-time-picker
+                v-model="siteForm.effectiveTo"
+                class="input-text"
+                format="HH:mm"
+                value-format="HH:mm"/>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :md="12" :lg="8">
+            <el-form-item
+              label="Idle Fee:"
+              label-width="130px"
+              prop="idleFee">
+              <el-input
+                v-model="siteForm.idleFee"
+                class="input-text"
+                oninput="value=value.replace(/[^\d.]/g, '').replace(/\.{2,}/g, '.').replace('.', '$#$').replace(/\./g, '').replace('$#$', '.').replace(/^(\-)*(\d+)\.(\d\d).*$/, '$1$2.$3').replace(/^\./g, '')"
+                maxlength="6"/>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :md="12" :lg="8">
+            <el-form-item
+              label="Every (minute):"
+              label-width="130px"
+              prop="everyMinute">
+              <el-input
+                maxlength="4"
+                v-model.number="siteForm.everyMinute"
+                class="input-text"/>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :md="12" :lg="8">
+            <el-form-item
+              label="Effective Date:"
+              label-width="130px"
+              prop="effectiveDate">
+              <el-date-picker
+                v-model="siteForm.effectiveDate"
+                class="input-text"
+                format="yyyy-MM-dd"
+                value-format="yyyy-MM-dd"/>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+      
+      <div class="view-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="handleClickSaveButton">
+              Save
+          </el-button>
+        </div>
+        <div
+          class="update-by"
+          v-if="isEdit">
+          <span
+            class="add-text"
+            :title='"CREATED BY " + siteForm.createdBy + " ON " + siteForm.createdOn'>
+            LAST UPDATED BY {{siteForm.updatedBy}} TIMESTAMP: {{siteForm.updatedOn}}
+          </span>
+        </div>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import settings from '../../settings.js'
+import Summary from '../site/components/Summary'
+import Dashboard from '../site/components/Dashboard'
+import ChargeRate from '../site/components/ChargeRate'
+import Balancing from '../site/components/Balancing'
+import EmailRecipient from '../site/components/EmailRecipient'
+import Reservation from '../site/components/Reservations'
+import WhiteListFleet from '../site/components/WhiteListFleet'
+import WhiteListUser from '../site/components/WhiteListUser'
+import SiteTypeWithTime from './components/SiteTypeWithTime'
+import waves from '@/directive/waves' // waves directive
+import site from '../../http/api/site'
+import { getRoleName } from '@/utils/auth'
+export default {
+  directives: { waves },
+  components: {
+    Balancing,
+    ChargeRate,
+    Dashboard,
+    EmailRecipient,
+    Reservation,
+    WhiteListUser,
+    WhiteListFleet,
+    SiteTypeWithTime,
+    Summary
+  },
+  data() {
+    return {
+      pageLoading: false,
+      isMCSTUser: false,
+      countryOptions: [],
+      siteForm: {
+        sitePk: "",
+        siteName: "",
+        siteTypes: [],
+        emails: [],
+        carParkCode: "",
+        address: {
+          city: "",
+          countryCode: settings.defaultCountry,
+          street: "",
+          zipCode: ""
+        },
+        ocppSetting: {
+          expireHours: '',
+          ocppSettingId: '',
+          heartbeatIntervalMinutes: ''
+        },
+        free: false,
+        endlessService: false,//24/7
+        additionalNotes: "",
+        //罚款部分
+        enableIdleFee: false,//idle Fee
+        idleFee: "",//idle Fee
+        everyMinute: "",//idle Fee
+        effectiveDate: "",//idle Fee
+        effectiveFrom: "",//idle Fee
+        effectiveTo: "",//idle Fee
+        //预订部分
+        enableReservation: false,//预订
+        timeLimit: null,//预订
+        
+        parkingFee: "",
+        whitelistUser: [],
+        groupWhitelist: [],
+        operatingHours: "",
+        locationLatitude: "",
+        locationLongitude: "",
+        chargeRates: [],
+        specialChargeRates: [],
+        loadBalancing: "",//负载均衡
+        staticMaxAmpere: "",//负载均衡
+        siteChargingProfiles: null,//负载均衡
+      },
+      ratesForm: {
+        chargeRates: [{
+          rate: '',
+          rateType: '',
+          chargeTypePk: ''
+        }],
+        specialChargeRates: [{
+          rate: '',
+          rateType: '',
+          chargeTypePk: '',
+          groupPk: ''
+        }]
+      },
+      balancingForm: {
+        loadBalancing: 'dynamic',
+        staticMaxAmpere: 0,
+        siteChargingProfiles: [{
+          boxInUse: '',
+          chargingProfilePk: ''
+        }],
+      },
+      currencyData: {
+        SG: "S$"
+      },
+      autosize: {
+        minRows: 3,
+        maxRows: 6,
+      },
+      summaryInfo: {},
+      rules: {
+        siteName: {
+          required: true,
+          trigger: 'blur',
+          message: 'Site Name is required',
+        },
+        locationLatitude: [{
+          required: true,
+          trigger: 'blur',
+          message: 'Latitude is required',
+        }, {
+          pattern: /^[\-\+]?((0|([1-8]\d?))(\.\d{1,8})?|90(\.0{1,8})?)$/,
+          trigger: 'blur',
+          message: 'Please type a correct latitude',
+        }],
+        locationLongitude: [{
+          required: true,
+          trigger: 'blur',
+          message: 'Longitude is required',
+        }, {
+          pattern: /^[\-\+]?(0(\.\d{1,8})?|([1-9](\d)?)(\.\d{1,8})?|1[0-7]\d{1}(\.\d{1,8})?|180\.0{1,8})$/,
+          trigger: 'blur',
+          message: 'Please type a correct longitude',
+        }],
+        address: {
+          street: {
+            required: true,
+            trigger: 'blur',
+            message: 'Address is required',
+          },
+          zipCode: {
+            required: true,
+            trigger: 'blur',
+            message: 'Postal Code is required',
+          },
+          city: {
+            required: true,
+            trigger: 'blur',
+            message: 'City is required',
+          },
+          countryCode: {
+            required: true,
+            trigger: 'blur',
+            message: 'Country is required',
+          }
+        },
+        idleFee: {
+          required: true,
+          trigger: 'blur',
+          message: 'Idle fee is required'
+        },
+        effectiveDate: {
+          required: true,
+          trigger: 'change',
+          message: 'Effective Date is required'
+        },
+        effectiveFrom: {
+          required: true,
+          trigger: 'change',
+          message: 'Effective From is required'
+        },
+        effectiveTo: {
+          required: true,
+          trigger: 'change',
+          message: 'Effective To is required'
+        },
+        everyMinute: [{
+          required: true,
+          trigger: 'blur',
+          message: 'Every is required'
+        }, {
+          type: 'number',
+          message: 'Every is a number'
+        }]
+      },
+      enablePublic: false,
+      enableWhitelistUser: false,
+      enableWhitelistFleet: false,
+      isEdit: false
+    }
+  },
+  created() {
+    this.pageLoading = true;
+    this.isMCSTUser = getRoleName() === "MCST";
+    this.getCountryOptions()
+    if (this.$route.params.id) {
+      this.isEdit = true;
+      this.getSiteInfo()
+    } else {
+      setTimeout(() => {
+        this.pageLoading = false;
+      }, 500);
+    }
+  },
+  methods: {
+    getCountryOptions() {
+      site.getCountryList().then(res => {
+        if (res.data) {
+          this.countryOptions = res.data
+          const sign = {}
+          res.data.forEach(item => {
+            sign[item.value] = item.currencySymbol
+          })
+          this.currencyData = sign;
+        }
+      })
+    },
+    getSiteInfo() {
+      site.getSiteInfo({
+        sitePk: this.$route.params.id,
+      }).then(res => {
+        if (res.data) {
+          this.siteForm = res.data;
+          this.applySiteInfo();
+        } else {
+          this.pageLoading = false;
+        }
+      }).catch(() => {
+        this.$message({
+          message: err,
+          type: 'error',
+          duration: 3000,
+        })
+        this.pageLoading = false;
+      })
+    },
+    applySiteInfo() {
+      setTimeout(() => {
+        this.pageLoading = false;
+      }, 500);
+      if (this.siteForm.chargeRates && this.siteForm.chargeRates.length) {
+        const rate = [], srate = []
+        this.siteForm.chargeRates.forEach(item => {
+          if (item.groupPk) {
+            srate.push(item)
+          } else {
+            rate.push(item)
+          }
+        })
+        if (rate.length > 0) {
+          this.ratesForm.chargeRates = rate;
+        }
+        if (srate.length > 0) {
+          this.ratesForm.specialChargeRates = srate;
+        }
+      }
+      if (this.siteForm.siteTypes && this.siteForm.siteTypes.length) {
+        for (let type of this.siteForm.siteTypes) {
+          if (type.siteType == "PRI_USR_WHI") {
+            this.enableWhitelistUser = (type.allDay || (type.startTime && type.endTime))
+          } else if (type.siteType == "PRI_FLE_WHI") {
+            this.enableWhitelistFleet = (type.allDay || (type.startTime && type.endTime))
+          } else if (type.siteType == "PUB") {
+            this.enablePublic = (type.allDay || (type.startTime && type.endTime))
+          } else {
+            continue;
+          }
+        }
+      }
+      if (this.siteForm.siteSummary) {
+        this.summaryInfo = Object.assign(this.siteForm.siteSummary, {});
+        delete this.siteForm.siteSummary;
+      }
+      this.balancingForm = {
+        loadBalancing: this.siteForm.loadBalancing || 'dynamic',
+        staticMaxAmpere: this.siteForm.staticMaxAmpere || 0,
+        siteChargingProfiles: this.siteForm.siteChargingProfiles || [{
+          boxInUse: '',
+          chargingProfilePk: ''
+        }]
+      }
+    },
+    changeEndless(value) {
+      if (value) {
+        this.siteForm.operatingHours = ""
+      }
+    },
+    changeParkingFree(value) {
+      if (value) {
+        this.siteForm.parkingFee = ""
+      }
+    },
+    changeSiteTypes(data) {
+      if (this.isEdit && this.pageLoading) {
+        console.log("changeSiteTypes", data);
+        return;
+      }
+      this.siteForm.siteTypes = data;
+      /*for (let type of data) {
+        if (type.siteType == "PRI_USR_WHI") {
+          this.enableWhitelistUser = (type.allDay || (type.startTime && type.endTime))
+        } else if (type.siteType == "PRI_FLE_WHI") {
+          this.enableWhitelistFleet = (type.allDay || (type.startTime && type.endTime))
+        } else if (type.siteType == "PUB") {
+          this.enablePublic = (type.allDay || (type.startTime && type.endTime))
+        } else {
+          continue;
+        }
+      }*/
+    },
+    handleClickCancleButton() {
+      this.$router.push({
+        path: "/site-management-v2/site-configuration"
+      });
+    },
+    handleClickSaveButton() {
+      this.$refs['addForm'].validate(result => {
+        if (result) {
+          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 rate",
+              type: 'error',
+              duration: 3000,
+            })
+            this.scrollToView("idChargeRates", true);
+            return;
+          }*/
+          if (!this.siteForm.enableReservation) {
+            this.siteForm.timeLimit = "";
+          }
+          /*const chargeProfiles = []
+          this.balancingForm.siteChargingProfiles.forEach(item => {
+            if (item.boxInUse && item.chargingProfilePk)
+              chargeProfiles.push(item)
+          })
+          this.siteForm.siteChargingProfiles = chargeProfiles
+          if (this.balancingForm.loadBalancing == 'dynamic') {
+            this.siteForm.staticMaxAmpere = ""
+          } else {
+            this.siteForm.staticMaxAmpere = this.balancingForm.staticMaxAmpere
+          }
+          this.siteForm.loadBalancing = this.balancingForm.loadBalancing*/
+          /*if (!this.enablePublic && !this.enableWhitelistFleet && !this.enableWhitelistUser) {
+            this.$message({
+              message: "Please set time configuration",
+              type: 'error',
+              duration: 3000,
+            })
+            this.scrollToView("idSiteTypes");
+            return;
+          }*/
+          if (this.enableWhitelistUser) {
+            let _list = []
+            this.siteForm.whitelistUser.forEach(item => {
+              if (item.mobileNumber && item.licenceNumber) {
+                _list.push(item);
+              }
+            })
+            if (_list.length == 0) {
+              this.$message({
+                message: "Please add at least one whitelist user",
+                type: 'error',
+                duration: 3000,
+              })
+              return;
+            } else {
+              this.siteForm.whitelistUser = _list;
+            }
+          } else {
+            this.siteForm.whitelistUser = [];
+          }
+          if (this.enableWhitelistFleet) {
+            let _list = []
+            this.siteForm.groupWhitelist.forEach(item => {
+              if (item.groupPk) {
+                _list.push(item);
+              }
+            })
+            if (_list.length == 0) {
+              this.$message({
+                message: "Please add at least one whitelist group",
+                type: 'error',
+                duration: 3000,
+              })
+              return;
+            } else {
+              this.siteForm.groupWhitelist = _list;
+            }
+          } else {
+            this.siteForm.groupWhitelist = []
+          }
+          this.pageLoading = true;
+          const params = {
+            ...this.siteForm,
+            chargeRates: rates
+          }
+          console.log("form", params);
+          if (this.isEdit) {
+            this.updateSite(params);
+          } else {
+            this.addSite(params)
+          }
+          //this.editSiteWithForm();
+        } else {
+          this.scrollToView("idSiteInfo", true);
+        }
+      })
+    },
+    scrollToView(id, alignToTop) {
+      //console.log("scrollToView", !alignToTop);
+      window.requestAnimationFrame(() => {
+        document.getElementById(id).scrollIntoView(!alignToTop, {
+          block: "start",
+          behavior: "smooth"
+        })
+      });
+    },
+    isSiteTypeEnable(index, isSwitch) {
+      for (let i = 0; i < this.siteForm.siteTypes.length; i++) {
+        let type = this.siteForm.siteTypes[i];
+        if (type.allDay) {
+          if (index == i && isSwitch) {
+            return false;
+          } else {
+            return true;
+          }
+        } else {
+          continue;
+        }
+      }
+      return false;
+    },
+    addSite(params) {
+      site.addSite(params).then(res => {
+        this.$message({
+          message: "Add site success",
+          type: 'success',
+          duration: 3000,
+        })
+        this.handleClickCancleButton();
+      }).catch((error) => {
+        this.$message({
+          message: error,
+          type: "error",
+          duration: 5000,
+        })
+      }).finally(() => {
+        this.pageLoading = false;
+      })
+    },
+    updateSite(params) {
+      site.updateSite(params).then(res => {
+        this.$message({
+          message: "Update site success",
+          type: 'success',
+          duration: 3000,
+        })
+        this.handleClickCancleButton();
+      }).catch((error) => {
+        this.$message({
+          message: error,
+          type: "error",
+          duration: 5000,
+        })
+      }).finally(() => {
+        this.pageLoading = false;
+      })
+    },
+    configLoadBalance() {
+      this.$openRoute("/smart-energy-management/site-load-balancing/" + this.siteForm.sitePk);
+    }
+  }
+}
+</script>
+
+<style scoped lang='scss'>
+  @import '../../styles/element-ui.scss';
+  @import '../../styles/variables.scss';
+  
+  .view-container {
+    width: 100%;
+    padding: 20px 60px;
+    min-height: $mainAppMinHeight;
+    background-color: #F0F5FC;
+  }
+  
+  .view-content {
+    margin: 0 8px 16px;
+    padding: 15px 50px;
+    border-radius: 6px;
+    background-color: white;
+  }
+  
+  @media screen and (max-width: 800px) {
+    .view-container {
+      padding: 10px 20px;
+    }
+    .view-content {
+      padding: 15px 40px;
+    }
+  }
+  
+  @media screen and (max-width: 500px) {
+    .view-container {
+      padding: 0px;
+    }
+    .view-content {
+      padding: 15px 30px;
+    }
+  }
+  
+  .sparator {
+    margin: 10px -30px;
+    height: 2px;
+    background-color: #F0F5FC;
+  }
+  
+  .section-title {
+    color: #333333;
+    margin-top: 20px;
+    margin-bottom: 30px;
+    font-size: 15px;
+    line-height: 24px;
+    font-weight: 700;
+    font-family: sans-serif;
+    text-transform: uppercase;
+  }
+  
+  .section-sub-title {
+    font-size: 14px;
+    padding-left: 5px;
+    font-weight: normal;
+  }
+  
+  .site-type-layout {
+    overflow-x: auto;
+  }
+  
+  .site-type-layout::-webkit-scrollbar-track-piece {
+    background-color:#f8f8f8; 
+  }
+  
+  .site-type-layout::-webkit-scrollbar {
+    width: 7px;
+    height: 10px;
+  }
+  .site-type-layout::-webkit-scrollbar-thumb {
+    transition: all .4s;
+    border-radius: 30px;
+    background-color: rgba($--color-primary, 0.3);
+    background-clip: padding-box;
+  }
+  .site-type-layout::-webkit-scrollbar-thumb:hover {
+    background-color: rgba($--color-primary, 0.6);
+  }
+  
+  .input-text {
+    width: 100% !important;
+    max-width: 300px;
+  }
+  
+  .add-text {
+    width: 100%;
+    font-size: 14px;
+    max-width: 300px;
+  }
+  
+  .add-text ::v-deep .el-textarea__inner,
+  .input-text ::v-deep .el-textarea__inner {
+    font-family: sans-serif;
+  }
+  
+  .value-text {
+    width: 100%;
+    min-width: 100px;
+    max-width: 200px;
+  }
+  
+  .value-unit {
+    padding-left: 10px;
+  }
+  
+  .cancel-button {
+    width: 94px;
+    height: 40px;
+  }
+  
+  .confirm-button {
+    width: 94px;
+    height: 40px;
+    margin-left: 20px;
+    background: $--color-primary;
+    border: 1px solid $--color-primary;
+    box-sizing: border-box;
+    border-radius: 4px;
+  }
+  
+  ::v-deep .el-switch__label.el-switch__label--left span {
+    font-size: 14px;
+    color: black;
+  }
+  .idle-switch {
+    font-size: 13px;
+    text-transform: none;
+  }
+  .tips {
+    color: #999;
+    font-size: 12px;
+    padding-left: 150px;
+    padding-bottom: 20px;
+  }
+  .tips i {
+    color: #333;
+    font-weight: 500;
+    font-style: normal;
+  }
+  .new-load-balance {
+    position: relative;
+    padding-left: 15px;
+    padding-bottom: 20px;
+  }
+  .new-text {
+    top: -10px;
+    left: 0px;
+    color: #FF1122;
+    font-size: 12px;
+    position: absolute;
+    transform: scale(0.8);
+  }
+</style>

+ 331 - 0
Strides-Admin/src/views/site2/index.vue

@@ -0,0 +1,331 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <div class="filter-view">
+        <el-select
+          class="filter-view-item"
+          v-model="filter.pageVo.providerPk"
+          placeholder="Service Provider"
+          @change="handleFilter"
+          clearable>
+          <el-option
+            v-for="(item, index) in providerOptions"
+            :key="index"
+            :label="item.providerName"
+            :value="item.providerPk"/>
+        </el-select>
+        <div class="flex1" style="max-width: 350px;">
+          <el-input
+            class="filter-view-item"
+            v-model="filter.pageVo.criteria"
+            prefix-icon="el-icon-search"
+            placeholder="Search by Site Name, Address, Carpark Code"
+            @keyup.enter.native="handleFilter"
+            @change="handleFilter"
+            clearable/>
+        </div>
+        <div>
+          <el-button
+            v-waves
+            type="primary"
+            icon="el-icon-search"
+            @click="handleFilter">
+            Search
+          </el-button>
+        </div>
+        <div class="filter-flex-button">
+          <el-button
+            v-if="visible.addButton"
+            type="primary"
+            icon="el-icon-plus"
+            @click="handleClickAddSiteButton">
+              Add Site
+          </el-button>
+        </div>
+      </div>
+    </div>
+    <el-table
+      :key="tableKey"
+      v-loading="listLoading"
+      :data="list"
+      style="width: 100%;">
+      <!--el-table-column
+        label="Site ID"
+        prop="sitePk"
+        align="center"
+        width="80">
+          <template slot-scope="{row}">
+            <span>{{ row.sitePk }}</span>
+          </template>
+      </el-table-column-->
+      <el-table-column
+        label="Site Name"
+        prop="siteName"
+        align="center"
+        min-width="260">
+          <template slot-scope="{row}">
+            <span class="link-type" @click="handleUpdateSite(row)">{{ row.siteName }}</span>
+          </template>
+      </el-table-column>
+      <el-table-column
+        label="Carpark Code"
+        prop="carParkCode"
+        align="center"
+        min-width="125"/>
+      <el-table-column
+        label="Address"
+        prop="siteAddress"
+        align="center"
+        min-width="280">
+        <template slot-scope="{row}">
+          <span>{{ row.siteAddress }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="Service Provider"
+        prop="siteName"
+        align="center"
+        min-width="180">
+        <template slot-scope="{row}">
+          <div
+            v-for="item in row.serviceProviders"
+            :key="item">
+            {{item}}
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="Rates"
+        prop="siteName"
+        align="center"
+        min-width="200">
+        <template slot-scope="{row}" v-if="row.dynamicRates">
+          <div
+            v-for="(item,index) in row.dynamicRates"
+            :key="index"
+            style="cursor: alias;"
+            @click="viewRateDialog(row)">
+            {{item}}
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="Total Stations"
+        prop="stationNo"
+        align="center"
+        min-width="100">
+          <template slot="header" slot-scope="scope">
+            <div>Stations</div>
+            <div style="font-size: 0.8em; margin-top: -5px;">(In Use/Total)</div>
+          </template>
+          <template slot-scope="{row}">
+            <span>{{ row.stationInUse || "0" }}/{{ row.stationNo || "0" }}</span>
+          </template>
+      </el-table-column>
+      <el-table-column
+        label="Total Connectors"
+        prop="connectorNo"
+        align="center"
+        min-width="100"
+        class-name="fixed-width">
+          <template slot="header" slot-scope="scope">
+            <div>Connectors</div>
+            <div style="font-size: 0.8em; margin-top: -5px;">(In Use/Total)</div>
+          </template>
+          <template slot-scope="{row}">
+            <span>{{ row.connectorInUse || "0" }}/{{ row.connectorNo || "0"}}</span>
+          </template>
+      </el-table-column>
+      <!--el-table-column
+        label="Connectors in Use"
+        prop="connectorNo"
+        align="center"
+        class-name="fixed-width">
+          <template slot-scope="{row}">
+            <span>{{ row.connectorInUse || "0" }}</span>
+          </template>
+      </el-table-column-->
+      <el-table-column
+        v-if="visible.actions"
+        label="Action"
+        align="center"
+        min-width="70">
+          <template slot-scope="{row, $index}">
+            <!-- <TableAction
+              :showDel="visible.actionDelete"
+              @edit="handleUpdateSite(row, $index)"
+              @delete="handleDeleteSite(row, $index)"/> -->
+            <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="handleUpdateSite">
+                  Edit
+                </el-dropdown-item>
+                <el-dropdown-item
+                  command="handleDeleteSite">
+                  Delete
+                </el-dropdown-item>
+                <el-dropdown-item
+                  command="viewRateDialog">
+                  View Rate
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </el-dropdown>
+          </template>
+      </el-table-column>
+    </el-table>
+    <div class="right">
+      <pagination
+        v-show="total > 0"
+        :total="total"
+        :page.sync="filter.pageNo"
+        :limit.sync="filter.pageSize"
+        @pagination="getList" />
+    </div>
+    <history-rate
+      v-bind="actionRate"
+      @hide="hideRateDialog"
+    />
+  </div>
+</template>
+
+<script>
+
+// waves directive
+import waves from '@/directive/waves'
+// secondary package based on el-pagination
+import Pagination from '@/components/Pagination'
+import TableAction from '@/components/TableAction.vue'
+import site from '../../http/api/site'
+import provider from '../../http/api/provider'
+import { getRoleName } from '@/utils/auth'
+import historyRate from '../site/historyRate.vue'
+
+export default {
+  name: 'SiteManagement',
+  components: { Pagination, TableAction, historyRate },
+  directives: { waves },
+  data() {
+    return {
+      tableKey: 0,
+      list: null,
+      total: 0,
+      listLoading: true,
+      filter: {
+        pageNo: 1,
+        pageSize: 10,
+        pageVo: {
+          criteria: '',
+          providerPk: ''
+        }
+      },
+      providerOptions: [],
+      visible: {
+        actions: true,
+        actionDelete: true,
+        addButton: true,
+      },
+      actionRate: {
+        id: 0,
+        visible: false
+      }
+    }
+  },
+  created() {
+    this.getAllProvider();
+    this.getList();
+    this.setVisible();
+  },
+  methods: {
+    setVisible() {
+      const role = getRoleName()
+      if (role === "MCST") {
+        this.visible.actionDelete = false
+        this.visible.addButton = false
+      } else if (this.$route.meta.onlyView) {
+        this.visible.actions = false
+        this.visible.addButton = false
+      }
+    },
+    getAllProvider() {
+      provider.getAllServiceProvider().then(res => {
+        if (res.data && res.data.length > 0) {
+          this.providerOptions = res.data
+        }
+      });
+    },
+    getList() {
+      this.listLoading = true
+      site.getSiteList(this.filter).then(res => {
+        if (res.data && res.total) {
+          this.list = res.data
+          this.total = res.total
+        } else {
+          this.list = []
+          this.total = 0
+        }
+      }).catch(err => {
+        this.$message({
+          message: err,
+          type: 'error'
+        })
+        this.list = []
+        this.total = 0
+      }).finally(() => {
+        this.listLoading = false
+      })
+    },
+    handleFilter() {
+      this.filter.pageNo = 1
+      this.getList()
+    },
+    handleClickAddSiteButton() {
+      //this.$router.push({ path: '/site-management/add-site' })
+      this.$router.push({ path: '/site-management-v2/add' })
+    },
+    handleUpdateSite(row) {
+      /*this.$store.commit('site/SET_SELECTED_SITE', row)
+      this.$router.push({ path: '/site-management/update-site' })*/
+      this.$router.push({ path: '/site-management-v2/edit/' + row.sitePk })
+    },
+    handleDeleteSite(row) {
+      this.$confirm(
+        'Are you sure you want to delete this site ?',
+        'Delete', {
+        confirmButtonText: 'OK',
+        cancelButtonText: 'Cancel',
+        type: 'warning'
+      }).then(res => {
+        site.deleteSite({ sitePk: row.sitePk }).then(() => {
+          this.$message({
+            message: `delete site success ${row.siteName}`,
+            type: "success",
+            duration: 3000,
+          })
+          this.list.splice(index, 1)
+        }).catch(error => {
+          this.$message({
+            message: error,
+            type: "error",
+            duration: 3000,
+          })
+        })
+      })
+    },
+    handleCommand(cb, item) {
+      this[cb](item)
+    },
+    viewRateDialog(row) {
+      this.actionRate.visible = true;
+      this.actionRate.id = row.sitePk;
+    },
+    hideRateDialog() {
+      this.actionRate.visible = false;
+    }
+  }
+}
+</script>
+