Эх сурвалжийг харах

Develop points management
https://dev.wormwood.com.sg/zentao/task-view-151.html

vbea 2 жил өмнө
parent
commit
cb374b90aa

+ 33 - 0
Strides-Admin/src/api/apiBase.js

@@ -0,0 +1,33 @@
+import { get } from '../http/http'
+
+const prefix = "dawn/api/v1/"
+
+const apiBase = {
+  getAssignStatusOptions() {
+    return get(prefix + "assignment-status-select")
+  },
+  getChargerTypeOptions() {
+    return get(prefix + "charger-type-select")
+  },
+  getUserGroupOptions(groupType) {
+    return get(prefix + "group-select", {groupType: groupType})
+  },
+  getUserTypeOptions() {
+    return get(prefix + "user-type-select")
+  },
+  /**
+   * 获取用户列表(模糊查询)
+   * @param {Object} data {email}
+   */
+  getUserOptions(data) {
+    return get(prefix + "user-select", data)
+  },
+  getUserTypeOptions() {
+    return get(prefix + "user-type-select")
+  },
+  getCountryList() {
+    return get(prefix + "country-select")
+  }
+}
+
+export default apiBase;

+ 50 - 0
Strides-Admin/src/api/points.js

@@ -0,0 +1,50 @@
+import {get, post, put, del} from '../http/http'
+
+const prefix = "dawn/api/v1/"
+
+const points = {
+  getPointsConfigPages(data) {
+    return post(prefix + "dynamic-points-pages", data)
+  },
+  getBasePointsConfig() {
+    return get(prefix + "base-dynamic-points")
+  },
+  saveBasePointsConfig(data) {
+    return post(prefix + "base-dynamic-points", data)
+  },
+  createPointsConfig(data) {
+    return post(prefix + "dynamic-points", data)
+  },
+  updatePointsConfig(data) {
+    return put(prefix + "dynamic-points", data)
+  },
+  viewPointsConfig(dynamicPointsId) {
+    return get(prefix + "dynamic-points/" + dynamicPointsId)
+  },
+  deletePointsConfig(dynamicPointsId) {
+    return del(prefix + "dynamic-points/" + dynamicPointsId)
+  },
+  deletePointsConfigItem(dynamicPointsItemId) {
+    return del(prefix + "dynamic-points-items/" + dynamicPointsItemId)
+  },
+  getSiteAssignmentPages(data) {
+    return post(prefix + "dynamic-points-site-pages", data)
+  },
+  getVehicleAssignmentPages(data) {
+    return post(prefix + "dynamic-points-vehicle_model-pages", data)
+  },
+  assignPointsSites(data) {
+    return post(prefix + "assigned-dynamic-points-site", data)
+  },
+  assignPointsVehicleModel(data) {
+    return post(prefix + "assigned-dynamic-points-vehicle_model", data)
+  },
+  unassignPointsSites(data) {
+    return post(prefix + "unassigned-dynamic-points-site", data)
+  },
+  unassignPointsVehicleModel(data) {
+    return post(prefix + "unassigned-dynamic-points-vehicle_model", data)
+  }
+}
+
+export default points;

+ 1 - 8
Strides-Admin/src/http/api/financial.js

@@ -91,14 +91,7 @@ const financial = {
   },
   viewCreditApply(creditActionId) {
     return get("dawn/api/v1/credit-action-application/" + creditActionId)
-  },
-  /**
-   * 获取用户列表(模糊查询)
-   * @param {Object} data {email}
-   */
-  getUserOptions(data) {
-    return get("dawn/api/v1/user-select", data)
-  },
+  }
   /*信用余额操作部分-End*/
 }
 

+ 54 - 0
Strides-Admin/src/router/PointsRouter.js

@@ -0,0 +1,54 @@
+import Layout from '@/layout'
+
+export default {
+  path: '/points-management',
+  component: Layout,
+  redirect: 'noRedirect',
+  meta: {
+    title: 'Points Management',
+    icon: 'financial'
+  },
+  alwaysShow: true,
+  children: [
+    {
+      path: '/points-management/setting',
+      component: () => import('@/views/points/index'),
+      name: 'dynamic-points',
+      meta: {
+        title: 'Dynamic Site-Points Setting',
+        icon: 'sidebar-submenu-item',
+        activeIcon: 'sidebar-submenu-item-active'
+      }
+    },
+    {
+      path: '/points-management/create',
+      component: () => import('@/views/points/detail'),
+      name: 'dynamic-points',
+      meta: {
+        title: 'Create',
+        icon: 'sidebar-submenu-item',
+        activeIcon: 'sidebar-submenu-item-active',
+        activeMenu: '/points-management/setting',
+        parent: {
+          title: 'Dynamic Site-Points Setting',
+          path: "/points-management/setting"
+        }
+      }
+    },
+    {
+      path: '/points-management/update/:id',
+      component: () => import('@/views/points/detail'),
+      name: 'dynamic-points',
+      meta: {
+        title: 'Update',
+        icon: 'sidebar-submenu-item',
+        activeIcon: 'sidebar-submenu-item-active',
+        activeMenu: '/points-management/setting',
+        parent: {
+          title: 'Dynamic Site-Points Setting',
+          path: "/points-management/setting"
+        }
+      }
+    }
+  ],
+}

+ 2 - 0
Strides-Admin/src/router/index.js

@@ -19,6 +19,7 @@ import SettingsRouter from './SettingsRouter'
 import additionalRoute from './addition'
 import MarketingRouter from './MarketingRouter'
 import VoucherRouter from './VoucherRouter'
+import PointsRouter from './PointsRouter'
 
 Vue.use(VueRouter)
 
@@ -81,6 +82,7 @@ const constantRoutes = [
   FinancialRouter,
   UserRouter,
   VoucherRouter,
+  PointsRouter,
   SupportRouter,
   OCPPRouter,
   ...ReportsRouter,

+ 2 - 1
Strides-Admin/src/views/financial/CreditActionDialog.vue

@@ -153,6 +153,7 @@
 </template>
 
 <script>
+import apiBase from '@/api/apiBase';
 import financial from '@/http/api/financial';
 export default {
   name: "CreditActionDialog",
@@ -261,7 +262,7 @@ export default {
       this.$emit("hide", h);
     },
     getUserOptions(word) {
-      financial.getUserOptions({
+      apiBase.getUserOptions({
         email: word
       }).then(res => {
         if (res.data) {

+ 399 - 0
Strides-Admin/src/views/points/AssignmentDialog.vue

@@ -0,0 +1,399 @@
+<template>
+  <el-dialog
+    :title="title"
+    :visible="visible"
+    :before-close="onHide"
+    custom-class="points-assign-dialog">
+    <div class="filter-container filter-view">
+      <el-select
+        style="min-width: 70px; max-width: 120px;"
+        clearable
+        v-model="filter.pageCriteria.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.pageCriteria.criteria"
+          placeholder="Search by Brand and Model"
+          @keyup.enter.native="onSearch"
+          v-if="isVehicle"/>
+        <el-input
+          clearable
+          v-model="filter.pageCriteria.criteria"
+          placeholder="Search by Site Name or Service Provider"
+          @keyup.enter.native="onSearch"
+          v-else/>
+      </div>
+      <el-button
+        type="primary"
+        @click="onSearch">
+        Search
+      </el-button>
+    </div>
+    <div class="assign-table-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"
+        v-if="visible">
+        <template v-if="isVehicle">
+          <el-table-column
+            align="center"
+            label="Brand"
+            prop="vehicleBrand"
+            min-width="120"/>
+          <el-table-column
+            align="center"
+            label="Model"
+            prop="vehicleModel"
+            min-width="120"/>
+        </template>
+        <template v-else>
+          <el-table-column
+            align="center"
+            label="Site Name"
+            prop="siteName"
+            min-width="130"/>
+          <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>
+        </template>
+        <el-table-column
+          align="center"
+          label="Assignment Status"
+          prop="assignmentStatus"
+          min-width="150"/>
+        <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.pageNum"
+        :limit.sync="filter.pageSize"
+        @pagination="getTableData"/>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import apiBase from '@/api/apiBase';
+import points from '@/api/points';
+import Pagination from '@/components/Pagination'
+export default {
+  name: "AssignmentDialog",
+  props: {
+    title: {
+      type: String,
+      default: "ASSIGN SITES"
+    },
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    isVehicle: {
+      type: Boolean,
+      default: false
+    },
+    pointsId: {
+      type: String,
+      default: ""
+    }
+  },
+  components: {Pagination},
+  data() {
+    return {
+      filter: {
+        pageNum: 1,
+        pageSize: 10,
+        pageCriteria: {
+          criteria: "",
+          dynamicPointsId: "",
+          assignmentStatus: ""
+        }
+      },
+      table: {
+        data: [],
+        total: 0,
+        loading: false
+      },
+      loading: {
+        assign: false,
+        unassign: false
+      },
+      selectRow: [],
+      statusOptions:[]
+    };
+  },
+  mounted() {
+    this.getStatusOptions();
+  },
+  watch: {
+    visible: {
+      handler(n, o) {
+        if (n) {
+          this.filter.pageCriteria.dynamicPointsId = this.pointsId;
+          this.onSearch();
+        }
+      }
+    }
+  },
+  methods: {
+    onHide() {
+      this.$emit("hide");
+    },
+    onSearch() {
+      this.filter.pageNo = 1;
+      this.getTableData();
+    },
+    getStatusOptions() {
+      apiBase.getAssignStatusOptions().then(res => {
+        if (res.data) {
+          this.statusOptions = res.data
+        }
+      }).catch(error => {
+        this.$message({
+          type: 'error',
+          message: error
+        })
+      })
+    },
+    getTableData() {
+      this.selectRow = []
+      this.table.loading = true;
+      const promise = this.isVehicle 
+        ? points.getVehicleAssignmentPages(this.filter)
+        : points.getSiteAssignmentPages(this.filter)
+      promise.then(res => {
+        if (res.data.totalRow && res.data.records) {
+          this.table.total = res.data.totalRow;
+          this.table.data = res.data.records;
+        } 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 => {
+        if (this.isVehicle) {
+          ids.push(item.vehicleModelId)
+        } else {
+          ids.push(item.sitePk)
+        }
+      })
+      return ids;
+    },
+    onClickAssign() {
+      if (this.isVehicle) {
+        this.assignVehicles();
+      } else {
+        this.assignSites();
+      }
+    },
+    onClickUnassign() {
+      if (this.isVehicle) {
+        this.unassignVehicles();
+      } else {
+        this.unassignSites();
+      }
+    },
+    assignSites() {
+      const params = {
+        dynamicPointsId: this.pointsId,
+        sitePks: this.getSelectIds()
+      }
+      this.loading.assign = true;
+      points.assignPointsSites(params).then(res => {
+        this.$message({
+          type: 'success',
+          message: res.msg || "Success"
+        })
+        this.getTableData()
+      }).catch(error => {
+        this.$message({
+          type: 'error',
+          message: error
+        })
+      }).finally(() => {
+        this.loading.assign = false;
+      })
+    },
+    unassignSites() {
+      const params = {
+        dynamicPointsId: this.pointsId,
+        sitePks: this.getSelectIds()
+      }
+      this.loading.unassign = true;
+      points.unassignPointsSites(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;
+      })
+    },
+    assignVehicles() {
+      const params = {
+        dynamicPointsId: this.pointsId,
+        vehicleModelIds: this.getSelectIds()
+      }
+      this.loading.assign = true;
+      points.assignPointsVehicleModel(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;
+      })
+    },
+    unassignVehicles() {
+      const params = {
+        dynamicPointsId: this.pointsId,
+        vehicleModelIds: this.getSelectIds()
+      }
+      this.loading.unassign = true;
+      points.unassignPointsVehicleModel(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 scoped>
+  >>> .points-assign-dialog {
+    width: 65vw;
+    height: 90vh;
+    display: flex;
+    max-width: 1200px;
+    flex-direction: column;
+    margin-top: 5vh !important;
+  }
+  >>> .points-assign-dialog .el-dialog__header {
+    padding: 20px 20px 0;
+    font-weight: bold;
+  }
+  >>> .points-assign-dialog .el-dialog__body {
+    flex: 1;
+    padding: 20px;
+    display: flex;
+    overflow: hidden;
+    flex-direction: column;
+  }
+  .assign-table-actions {
+    display: flex;
+    padding-top: 5px;
+    flex-wrap: wrap-reverse;
+    align-items: center;
+    justify-content: flex-end;
+  }
+  .points-assign-dialog .table-view {
+    flex: 1;
+    overflow-y: auto;
+    padding-top: 10px;
+    margin-bottom: -10px;
+  }
+  @media screen and (max-width: 1200px) {
+    .points-assign-dialog {
+      width: 70vw;
+    }
+  }
+  @media screen and (max-width: 1000px) {
+    .points-assign-dialog {
+      width: 80vw;
+    }
+  }
+  @media screen and (max-width: 800px) {
+    .points-assign-dialog {
+      width: 90vw;
+    }
+  }
+  @media screen and (max-width: 700px) {
+    .points-assign-dialog {
+      width: 99vw;
+    }
+  }
+  @media screen and (max-width: 320px) {
+    .points-assign-dialog {
+      width: 100%;
+      min-width: 300px;
+    }
+  }
+</style>

+ 193 - 0
Strides-Admin/src/views/points/BaseConfigDialog.vue

@@ -0,0 +1,193 @@
+<template>
+  <el-dialog
+    title="BASIC CONFIGURATION"
+    :visible="visible"
+    custom-class="basic-points-dialog"
+    :before-close="hideDialog">
+    <el-form
+      ref="baseConfigForm"
+      :model="form"
+      :rules="rules"
+      label-position="top">
+      <el-form-item
+        label="POINTS EARNING FORMULA:"
+        class="base-form-item"
+        prop="earningPoints">
+        <div class="flexc">
+          <el-input
+            class="input-text"
+            v-model="form.earningValue"/>
+          <div class="unit-text">SGD =</div>
+          <el-input
+            class="input-text"
+            v-model="form.earningPoints"/>
+          <div class="unit-text">POINTS</div>
+        </div>
+      </el-form-item>
+      <el-form-item
+        label="POINTS OFFSET FORMULA:"
+        class="base-form-item"
+        prop="offsetValue">
+        <div class="flexc">
+          <el-input
+            class="input-text"
+            v-model="form.offsetPoints"/>
+          <div class="unit-text">POINTS =</div>
+          <el-input
+            class="input-text"
+            v-model="form.offsetValue"/>
+          <div class="unit-text">SGD</div>
+        </div>
+      </el-form-item>
+      <div
+        class="flexcc"
+        style="padding-top: 60px;">
+        <el-button
+          class="cancel-button"
+          @click="hideDialog">
+          CANCEL
+        </el-button>
+        <el-button
+          type="primary"
+          @click="onSave"
+          :loading="loadingBtn">
+          &nbsp; SAVE &nbsp;
+        </el-button>
+      </div>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script>
+import points from '@/api/points';
+export default {
+  name: "BaseConfigDialog",
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    config: {
+      type: Object,
+      default: () => {}
+    }
+  },
+  watch: {
+    visible: {
+      handler(n, o) {
+        if (n) {
+          if (this.config.dynamicPointsId) {
+            this.form = {
+              ...this.config
+            }
+          }
+          this.$nextTick(() => {
+            this.$refs.baseConfigForm.clearValidate();
+          })
+        }
+      }
+    }
+  },
+  data() {
+    return {
+      loadingBtn: false,
+      form: {
+        earningValue: "",
+        earningPoints: "",
+        offsetPoints: "",
+        offsetValue: "",
+        dynamicPointsId: ""
+      },
+      pattern: /^[1-9]+\d*\.?\d*$/,
+      rules: {
+        earningPoints: {
+          required: false,
+          trigger: 'change',
+          validator: (rule, value, callback) => {
+            if (this.form.earningValue && this.form.earningPoints) {
+              if (this.pattern.test(this.form.earningValue) && this.pattern.test(this.form.earningPoints)) {
+                callback()
+              } else {
+                callback(new Error("Please enter a correct number"))
+              }
+            } else {
+              callback(new Error("Please enter points earning formula"))
+            }
+          }
+        },
+        offsetValue: {
+          required: false,
+          trigger: 'change',
+          validator: (rule, value, callback) => {
+            if (this.form.offsetPoints && this.form.offsetValue) {
+              if (this.pattern.test(this.form.offsetPoints) && this.pattern.test(this.form.offsetValue)) {
+                callback()
+              } else {
+                callback(new Error("Please enter a correct number"))
+              }
+            } else {
+              callback(new Error("Please enter points offset formula"))
+            }
+          }
+        }
+      }
+    };
+  },
+  mounted() {
+    
+  },
+  methods: {
+    hideDialog(e, h) {
+      this.$emit("hide", h);
+    },
+    onSave() {
+      this.$refs.baseConfigForm.validate((valid) => {
+        if (valid) {
+          this.updateBaseConfig();
+        }
+      })
+    },
+    updateBaseConfig() {
+      this.loadingBtn = true;
+      points.saveBasePointsConfig(this.form).then(res => {
+        this.loadingBtn = false;
+        this.$message({
+          message: res.msg,
+          type: 'success',
+        })
+        this.hideDialog(true, true);
+      }).catch(err => {
+        this.loadingBtn = false;
+        this.$message({
+          message: err,
+          type: 'error',
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+>>> .basic-points-dialog {
+  width: 100%;
+  padding: 0 10px;
+  max-width: 450px;
+}
+.base-form-item >>> .el-form-item__label {
+  color: #000;
+}
+.input-text {
+  width: 100%;
+  max-width: 120px;
+}
+.input-text >>> .el-textarea__inner {
+  font-family: sans-serif;
+}
+.unit-text {
+  color: #333;
+  padding: 0 8px;
+  font-size: 14px;
+  font-weight: bold;
+}
+</style>

+ 586 - 0
Strides-Admin/src/views/points/detail.vue

@@ -0,0 +1,586 @@
+<template>
+  <div class="container" v-loading="loading.page">
+    <el-form
+      ref="form"
+      :model="form"
+      :rules="rules"
+      label-width="110px"
+      label-position="right">
+      <div class="content">
+        <div class="section-title">POINTS CONFIGURATION</div>
+        <el-form-item
+          label="Name:"
+          prop="dynamicPointsName"
+          class="flex1">
+          <el-input
+            class="add-text"
+            v-model="form.dynamicPointsName"
+            maxlength="50"/>
+        </el-form-item>
+        <el-form-item
+          label="Country:"
+          prop="countryCode"
+          class="flex1">
+          <el-select
+            class="add-text"
+            v-model="form.countryCode"
+            @change="changeCountry">
+            <el-option
+              v-for="(item, index) in options.country"
+              :key="index"
+              :label="item.countryName"
+              :value="item.countryCode"/>
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          label="Valid Period:"
+          prop="periods"
+          class="flex1">
+          <el-date-picker
+            v-model="form.periods"
+            class="add-text"
+            type="datetimerange"
+            format="yyyy-MM-dd HH:mm"
+            value-format="yyyy-MM-dd HH:mm"
+            range-separator="-"
+            start-placeholder="Start Date"
+            end-placeholder="End Date"/>
+        </el-form-item>
+      </div>
+      <div class="content">
+        <div class="section-title">GROUP CONFIGURATION</div>
+        <el-table
+          :data="form.pointsItems"
+          class="config-item-table"
+          header-row-class-name="customer-row"
+          row-class-name="customer-row">
+          <el-table-column
+            label="USER TYPE:"
+            prop="label"
+            min-width="120">
+            <template slot-scope="{row}">
+              <el-select
+                v-model="row.userType"
+                @change="v => getGroupOptions(v, row)">
+                <el-option
+                  v-for="item in options.uTypes"
+                  :key="item"
+                  :label="item"
+                  :value="item">
+                </el-option>
+              </el-select>
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="GROUP NAME:"
+            prop="label"
+            min-width="140">
+            <template slot-scope="{row}">
+              <el-select
+                v-model="row.groupPk"
+                @change="getGroupOptions"
+                :disabled="groupOptions[row.userType].length == 0"
+                v-if="groupOptions[row.userType]">
+                <el-option
+                  v-for="(item, index) in groupOptions[row.userType]"
+                  :key="index"
+                  :label="item.key"
+                  :value="item.value">
+                </el-option>
+              </el-select>
+              <div class="center" v-else-if="row.userType">
+                <i class="el-icon-loading"/>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="CONNECTOR TYPE:"
+            prop="label"
+            min-width="160">
+            <template slot-scope="{row}">
+              <el-select
+                v-model="row.connectorType">
+                <el-option
+                  v-for="item in options.cTypes"
+                  :key="item"
+                  :label="item"
+                  :value="item">
+                </el-option>
+              </el-select>
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="POINTS EARNING FORMULA:"
+            prop="label"
+            min-width="300">
+            <template slot-scope="{row}">
+              <div class="flexc">
+                <el-input
+                  class="input-text"
+                  type="number"
+                  v-model="row.earningValue"/>
+                <div class="unit-text">{{currency}} =</div>
+                <el-input
+                  class="input-text"
+                  type="number"
+                  v-model="row.earningPoints"/>
+                <div class="unit-text">POINTS</div>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="POINTS OFFSET FORMULA:"
+            prop="label"
+            min-width="300">
+            <template slot-scope="{row}">
+              <div class="flexc">
+                <el-input
+                  class="input-text"
+                  type="number"
+                  v-model="row.offsetPoints"/>
+                <div class="unit-text">POINTS =</div>
+                <el-input
+                  class="input-text"
+                  type="number"
+                  v-model="row.offsetValue"/>
+                <div class="unit-text">{{currency}}</div>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="Action"
+            prop="label"
+            min-width="60">
+            <template slot="header">
+              <div class="flexcc">
+                <img
+                  class="list-item-icon"
+                  @click="addConfigItem"
+                  src="../../assets/form-list-add.png"/>
+              </div>
+            </template>
+            <template slot-scope="{row, $index}">
+              <div class="flexcc">
+                <img
+                  class="list-item-icon"
+                  @click="removeConfigItem(row, $index)"
+                  src="../../assets/form-list-sub.png"/>
+              </div>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+      <div class="content flexcr">
+        <div class="buttons">
+          <el-button
+            @click="onBack"
+            type="primary"
+            class="cancel-button">
+            CANCEL
+          </el-button>
+          <el-button
+            @click="onClickSave"
+            type="primary"
+            :loading="loading.save">
+            &nbsp; &nbsp;SAVE&nbsp; &nbsp;
+          </el-button>
+        </div>
+        <div class="update-by" v-if="isEdit">
+          <span
+            class="add-text"
+            :title='"CREATED BY " + form.createdBy + " ON " + form.createdOn'>
+            LAST UPDATED BY {{form.updatedBy}} TIMESTAMP: {{form.updatedOn}}
+          </span>
+        </div>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import apiBase from '@/api/apiBase';
+import points from '@/api/points';
+import settings from '../../settings';
+export default {
+  data() {
+    return {
+      loading: {
+        page: true,
+        save: false
+      },
+      isEdit: false,
+      currency: "SGD",
+      form: {
+        dynamicPointsId: "",
+        dynamicPointsName: "",
+        countryCode: settings.defaultCountry,
+        periods: "",
+        pointsItems: []
+      },
+      pointsItem: {
+        groupPk: "",
+        userType: "",
+        connectorType: "",
+        earningValue: "",
+        earningPoints: "",
+        offsetValue: "",
+        offsetPoints: "",
+        dynamicPointsItemId: ""
+      },
+      options: {
+        cTypes: [],
+        uTypes: [],
+        country: []
+      },
+      groupOptions: {
+        PUBLIC: []
+      },
+      rules: {
+        dynamicPointsName: {
+          required: true,
+          trigger: "blur",
+          message: "Please enter name"
+        },
+        countryCode: {
+          required: true,
+          trigger: "change",
+          message: "Please select country"
+        },
+        periods: {
+          required: true,
+          trigger: "change",
+          validator: (rule, value, callback) => {
+            if (value && value.length > 0) {
+              if (value[0] && value[1]) {
+                callback()
+              } else {
+                callback("Please select valid period")
+              }
+            } else {
+              callback("Please select valid period")
+            }
+          }
+        },
+      }
+    };
+  },
+  created() {
+    this.getCountryOptions(() => {
+      if (this.$route.params.id) {
+        this.isEdit = true;
+        this.getPointsConfigInfo();
+      } else {
+        this.addConfigItem();
+        this.changeCountry();
+      }
+    });
+  },
+  methods: {
+    init() {
+      if (this.form.periodStart && this.form.periodEnd) {
+        this.form.periods = [
+          this.form.periodStart,
+          this.form.periodEnd
+        ]
+      }
+      if (!this.form.pointsItems || this.form.pointsItems.length == 0) {
+        this.form.pointsItems = [];
+        this.addConfigItem();
+      } else {
+        this.form.pointsItems.forEach(item => {
+          this.getGroupOptions(item.userType, item, true)
+        });
+      }
+      this.changeTimePeriod();
+      this.changeCountry();
+    },
+    onBack() {
+      this.$nextTick(() => {
+        this.$router.replace({
+          path: "/points-management/setting"
+        })
+      })
+    },
+    changeTimePeriod() {
+      if (this.form.periods == null || this.form.periods == undefined) {
+        this.form.periods = ["", ""];
+      }
+    },
+    changeCountry() {
+      if (this.options.country && this.options.country.length > 0) {
+        for(let country of this.options.country) {
+          if (country.countryCode == this.form.countryCode) {
+            this.currency = country.currency;
+            break;
+          }
+        }
+      }
+    },
+    getCountryOptions(back) {
+      apiBase.getCountryList().then(res => {
+        if (res.data) {
+          this.options.country = res.data
+        } else {
+          this.options.country = []
+        }
+      }).catch(() => {
+        this.$message({
+          message: err,
+          type: 'error',
+          duration: 3000,
+        })
+        this.options.country = []
+      }).finally(() => {
+        back();
+        this.getChargeTypeOptions();
+        this.getUserTypeOptions();
+      })
+    },
+    getChargeTypeOptions() {
+      apiBase.getChargerTypeOptions().then(res => {
+        if (res.data) {
+          this.options.cTypes = res.data
+        } else {
+          this.options.cTypes = []
+        }
+      }).catch(() => {
+        this.$message({
+          message: err,
+          type: 'error',
+          duration: 3000,
+        })
+        this.options.cTypes = []
+      });
+    },
+    getUserTypeOptions() {
+      apiBase.getUserTypeOptions().then(res => {
+        if (res.data) {
+          this.options.uTypes = res.data
+        } else {
+          this.options.uTypes = []
+        }
+      }).catch(() => {
+        this.$message({
+          message: err,
+          type: 'error',
+          duration: 3000,
+        })
+        this.options.uTypes = []
+      }).finally(() => {
+        this.loading.page = false;
+      });
+    },
+    getGroupOptions(userType, row, init) {
+      if (!init) {
+        row.groupPk = "";
+      }
+      if (this.groupOptions[userType]) {
+        return;
+      }
+      apiBase.getUserGroupOptions(userType).then(res => {
+        if (res.data) {
+          this.groupOptions[userType] = res.data
+        } else {
+          this.groupOptions[userType] = []
+        }
+      }).catch(err => {
+        this.$message({
+          message: err,
+          type: 'error'
+        })
+        this.groupOptions[userType] = []
+      }).finally(() => {
+        row.userType = "";
+        this.$nextTick(() => {
+          row.userType = userType;
+          this.$forceUpdate();
+        });
+      })
+    },
+    getPointsConfigInfo() {
+      points.viewPointsConfig(this.$route.params.id).then(res => {
+        if (res.data) {
+          this.form = res.data;
+          this.init();
+        }
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      }).finally(() => {
+        this.loading.page = false;
+      })
+    },
+    addConfigItem() {
+      this.form.pointsItems.push({
+        ...this.pointsItem
+      });
+    },
+    removeConfigItem(item, index) {
+      if (item.dynamicPointsItemId) {
+        this.$confirm('Are you sure you want to remove this configuration item?', 'Delete', {
+          confirmButtonText: 'Confirm',
+          cancelButtonText: 'Cancel',
+          type: 'warning',
+        }).then(() => {
+          this.deleteConfigItem(item, index);
+        })
+      } else {
+        this.form.pointsItems.splice(index, 1);
+        if (this.form.pointsItems.length == 0) {
+          this.addConfigItem()
+        }
+      }
+    },
+    deleteConfigItem(item, index) {
+      points.deletePointsConfigItem(item.dynamicPointsItemId).then(res => {
+        this.form.pointsItems.splice(index, 1);
+        if (this.form.pointsItems.length == 0) {
+          this.addConfigItem()
+        }
+      }).catch(error => {
+        this.$notify({
+          type: 'error',
+          message: error,
+          title: 'Delete failed'
+        })
+      })
+    },
+    onClickSave() {
+      this.$refs.form.validate(result => {
+        if (result) {
+          this.loading.save = true;
+          this.isEdit ? this.updateConfig() : this.createConfig();
+        }
+      });
+    },
+    createConfig() {
+      points.createPointsConfig(this.form).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Successfully Created"
+        });
+        this.onBack();
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        });
+      }).finally(() => {
+        this.loading.save = false;
+      });
+    },
+    updateConfig() {
+      points.updatePointsConfig(this.form).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Successfully Updated"
+        });
+        this.onBack();
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        });
+      }).finally(() => {
+        this.loading.save = false;
+      });
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  @import '../../styles/variables.scss';
+  .container {
+    width: 100%;
+    padding: 20px 60px;
+    min-height: $mainAppMinHeight;
+    background-color: #F0F5FC;
+  }
+  .content {
+    min-width: 400px;
+    margin: 0 8px 16px;
+    padding: 15px 60px;
+    border-radius: 6px;
+    background-color: white;
+  }
+  .section-title {
+    color: #333;
+    margin-top: 20px;
+    margin-bottom: 30px;
+    font-size: 15px;
+    user-select: none;
+    line-height: 24px;
+    font-weight: bold;
+    font-family: sans-serif;
+    text-transform: uppercase;
+  }
+  .add-text {
+    width: 100%;
+    min-width: 200px;
+    max-width: 350px;
+  }
+  
+  .area-text {
+    width: 100%;
+    min-width: 200px;
+    max-width: 350px;
+  }
+  
+  .input-text {
+    flex: 1;
+  }
+  
+  .unit-text {
+    color: #333;
+    padding: 0 8px;
+    font-size: 12px;
+    font-weight: bold;
+    white-space: nowrap;
+  }
+  
+  .add-text ::v-deep .el-textarea__inner,
+  .area-text ::v-deep .el-textarea__inner {
+    font-family: sans-serif;
+  }
+  .buttons {
+    padding-top: 15px;
+    padding-bottom: 15px;
+  }
+  @media screen and (max-width: 500px) {
+    .container {
+      padding: 0px;
+    }
+    .content {
+      padding: 15px 30px;
+    }
+  }
+  .list-item-icon {
+    width: 30px;
+    height: 30px;
+    cursor: pointer;
+    font-size: 26px;
+    font-weight: 500;
+    user-select: none;
+  }
+  .config-item-table {
+    width: 100%;
+    padding-bottom: 10px;
+  }
+  .config-item-table:before {
+    height: 0;
+  }
+  ::v-deep .customer-row {
+    th,td {
+      padding: 0 0 10px;
+      min-height: 30px;
+      background: #fff;
+      border-bottom: none;
+    }
+    .time-picker {
+      width: 100%;
+    }
+  }
+</style>

+ 322 - 0
Strides-Admin/src/views/points/index.vue

@@ -0,0 +1,322 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container flexr" style="align-items: flex-end;overflow: hidden;">
+      <div class="flex2">
+        <div class="flexcr">
+          <div class="section-title">
+            BASE SETTING
+            <span
+              class="normal"
+              v-if="baseConfig.lastUpdateTime">
+              (LAST UPDATED: {{baseConfig.lastUpdateTime}})
+            </span>
+          </div>
+          <div
+            class="basic-edit"
+            v-if='roleName == "ADMIN"'
+            @click="editBaseConfig">
+            <i class="el-icon-edit-outline"/>
+            Edit
+          </div>
+        </div>
+        <div
+          class="basic-config"
+          v-if="baseConfig.dynamicPointsId">
+          <b>POINTS EARNING FORMULA:&nbsp;</b>
+          <span>{{baseConfig.earningValue}} {{baseConfig.earningCurrency}} = {{baseConfig.earningPoints}} POINTS</span>
+        </div>
+        <div
+          class="basic-config"
+          v-if="baseConfig.dynamicPointsId">
+          <b>POINTS OFFSET FORMULA:&nbsp;</b>
+          <span>{{baseConfig.offsetPoints}} POINTS = {{baseConfig.offsetValue}} {{baseConfig.offsetCurrency}}</span>
+        </div>
+      </div>
+      <div class="filter-flex-button" style="padding: 5px 0;">
+        <el-button
+          type="primary"
+          @click="onClickAdd"
+          icon="el-icon-plus"
+          v-if="!$route.meta.onlyView">
+          Add Point Config
+        </el-button>
+      </div>
+    </div>
+    <el-table
+      v-loading="table.loading"
+      :data="table.list">
+      <el-table-column
+        align="center"
+        label="Name"
+        min-width="150">
+        <template slot-scope="{row}">
+          <div
+            class="link-type"
+            @click="onClickEdit(row)">
+            {{row.dynamicPointsName}}
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="Country"
+        prop="countryName"
+        min-width="100"/>
+      <el-table-column
+        align="center"
+        label="Valid Period"
+        min-width="150">
+        <template slot-scope="{row}">
+          <div>Start: {{row.periodStart}}</div>
+          <div>End: {{row.periodEnd}}</div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="Sites"
+        prop="sites"
+        min-width="100"/>
+      <el-table-column
+        align="center"
+        label="Vehicle Types"
+        prop="vehicleTypes"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Status"
+        prop="dataStatus"
+        min-width="80"/>
+      <el-table-column
+        v-if="!$route.meta.onlyView"
+        label="Action"
+        align="center"
+        min-width="70">
+        <template slot-scope="{row, $index}">
+          <el-dropdown
+            class="action-dropdown"
+            @command="(v) => handleCommand(v, row)"
+            v-if="row.dataStatus == 'Active'">
+            <i class="el-icon-more icon-action"></i>
+            <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item
+                command="onClickAssignSite">
+                Assign Sites
+              </el-dropdown-item>
+              <el-dropdown-item
+                command="onClickAssignVehicle">
+                Assign Vehicles
+              </el-dropdown-item>
+              <el-dropdown-item
+                command="onClickEdit">
+                Edit
+              </el-dropdown-item>
+              <el-dropdown-item
+                command="onClickDelete">
+                Delete
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="right">
+      <pagination
+        v-show="table.total > 0"
+        :total="table.total"
+        :page.sync="filters.pageNum"
+        :limit.sync="filters.pageSize"
+        @pagination="getTableData" />
+    </div>
+    <BaseConfigDialog
+      :visible="showBasic"
+      :config="baseConfig"
+      @hide="hideBaseDialog"/>
+    <AssignmentDialog
+      v-bind="assignment"
+      @hide="hideAssignment"/>
+  </div>
+</template>
+
+<script>
+import apiBase from '@/api/apiBase';
+import points from '@/api/points';
+import settings from '../../settings';
+import AssignmentDialog from './AssignmentDialog';
+import BaseConfigDialog from './BaseConfigDialog';
+import Pagination from '@/components/Pagination';
+import {getRoleName} from '../../utils/auth.js'
+export default {
+  data() {
+    return {
+      table: {
+        list: [],
+        total: 0,
+        loading: false
+      },
+      filters: {
+        pageNum: 1,
+        pageSize: 10,
+        pageCriteria: {
+          criteria: "",
+          countryCode: ""//settings.defaultCountry
+        }
+      },
+      roleName: "",
+      showBasic: false,
+      baseConfig: {},
+      assignment: {
+        title: "",
+        pointsId: "",
+        visible: false,
+        isVehicle: false
+      }
+    };
+  },
+  components: {Pagination, AssignmentDialog, BaseConfigDialog},
+  created() {
+    this.getBasicConfig();
+    this.toSearch();
+    this.roleName = getRoleName();
+  },
+  methods: {
+    toSearch() {
+      this.filters.pageNum = 1
+      this.getTableData()
+    },
+    getBasicConfig() {
+      points.getBasePointsConfig().then(res => {
+        if (res.data && res.data.dynamicPointsId) {
+          this.baseConfig = res.data
+        }
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      })
+    },
+    getTableData() {
+      this.table.loading = true;
+      points.getPointsConfigPages(this.filters).then(res => {
+        if (res.data.totalRow && res.data.records) {
+          this.table.total = res.data.totalRow;
+          this.table.list = res.data.records;
+        } else {
+          this.table.total = 0;
+          this.table.list = [];
+        }
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        })
+        this.table.total = 0;
+        this.table.list = [];
+      }).finally(() => {
+        this.table.loading = false;
+      })
+    },
+    editBaseConfig() {
+      this.showBasic = true;
+    },
+    hideBaseDialog(e) {
+      this.showBasic = false;
+      if (e) {
+        this.getBasicConfig();
+      }
+    },
+    handleCommand(cb, item) {
+      this[cb](item)
+    },
+    onClickAssignSite(item) {
+      this.assignment.isVehicle = false;
+      this.assignment.pointsId = item.dynamicPointsId;
+      this.assignment.title = "ASSIGN SITES (" + item.dynamicPointsName +  ")";
+      this.assignment.visible = true;
+    },
+    onClickAssignVehicle(item) {
+      this.assignment.isVehicle = true;
+      this.assignment.pointsId = item.dynamicPointsId;
+      this.assignment.title = "ASSIGN VEHICLE TYPES (" + item.dynamicPointsName +  ")";
+      this.assignment.visible = true;
+    },
+    hideAssignment() {
+      this.assignment.visible = false;
+      this.getTableData();
+    },
+    onClickAdd() {
+      this.$router.push({
+        path: '/points-management/create'
+      });
+    },
+    onClickEdit(item) {
+      this.$router.push({
+        path: '/points-management/update/' + item.dynamicPointsId
+      });
+    },
+    onClickDelete(item) {
+      this.$confirm(
+        'Are you sure you want to delete this points config?',
+        'Delete', {
+        confirmButtonText: 'Confirm',
+        cancelButtonText: 'Cancel',
+        type: 'warning'
+      }).then(res => {
+        this.deletePointsConfig(item.dynamicPointsId);
+      });
+    },
+    deletePointsConfig(id) {
+      this.table.loading = true;
+      points.deletePointsConfig(id).then(res => {
+        this.$message({
+          message: 'Delete successfully!',
+          type: 'success'
+        });
+        this.getTableData();
+      }).catch(err => {
+        this.$message({
+          message: err,
+          type: 'error'
+        });
+        this.table.loading = false;
+      });
+    }
+  }
+}
+</script>
+
+<style scoped>
+.section-title {
+  color: #333;
+  margin-bottom: 20px;
+  font-size: 16px;
+  user-select: none;
+  line-height: 24px;
+  font-weight: bold;
+  white-space: nowrap;
+  font-family: sans-serif;
+  text-overflow: ellipsis;
+  text-transform: uppercase;
+}
+.section-title .normal {
+  font-weight: normal;
+}
+.basic-config {
+  color: #333;
+  font-size: 14px;
+  padding-bottom: 6px;
+}
+.basic-edit {
+  color: #3179E4;
+  cursor: pointer;
+  font-size: 15px;
+  margin-left: 15px;
+  margin-bottom: 15px;
+  user-select: none;
+  white-space: nowrap;
+}
+.basic-edit:hover,
+.basic-edit:active {
+  color: #ff0000;
+}
+</style>

+ 0 - 1
Strides-Admin/src/views/voucher/detail.vue

@@ -586,7 +586,6 @@ export default {
             this.form.timePeriodStart = this.form.timePeriod[0]
             this.form.timePeriodEnd = this.form.timePeriod[1]
           }
-          this.loading.save = true;
           this.isEdit ? this.updateVoucher() : this.createVoucher();
         }
       });