Parcourir la source

Development OCPI
https://dev.wormwood.com.sg/zentao/task-view-274.html

vbea il y a 1 an
Parent
commit
f5ccd29379

+ 56 - 0
Strides-Admin/src/http/api/ocpi.js

@@ -0,0 +1,56 @@
+import {del, get, post, put} from '../http'
+
+const prefix = "dawn/api/v1/"
+
+const ocpi = {
+  initRoamingOperators(data) {
+    return post(prefix + "init-roaming-operators", data)
+  },
+  assignPartyShareLocations(data) {
+    return post(prefix + "assign-party-share-locations", data)
+  },
+  assignSharePartyLocations(data) {
+    return post(prefix + "assign-share-party-locations", data)
+  },
+  unassignPartyShareLocations(data) {
+    return post(prefix + "unassign-party-share-locations", data)
+  },
+  unassignSharePartyLocations(credentialId) {
+    return post(prefix + "unassign-share-party-locations", data)
+  },
+  pagesPartyShareLocations(data) {
+    return post(prefix + "party-share-location-pages", data)
+  },
+  pagesSharePartyLocations(data) {
+    return post(prefix + "share-party-location-pages", data)
+  },
+  pagesRoamingOperator(data) {
+    return post(prefix + "roaming-operator-pages", data)
+  },
+  getRoamingOperatorInfo(credentialId) {
+    return get(prefix + "roaming-operators/" + credentialId)
+  },
+  deleteRoamingOperatorInfo(credentialId) {
+    return del(prefix + "roaming-operators/" + credentialId)
+  },
+  pagesSendConfig(data) {
+    return post(prefix + "send-config-pages", data)
+  },
+  getSendConfigTokens() {
+    return get(prefix + "send-config-tokens")
+  },
+  getSendConfigUrls() {
+    return get(prefix + "send-config-urls")
+  },
+  saveSendConfig(data) {
+    return post(prefix + "send-configs", data)
+  },
+  updateSendConfig(data) {
+    return put(prefix + "send-configs", data)
+  },
+  deleteSendConfig(credentialTokenId) {
+    return del(prefix + "send-configs/" + credentialTokenId)
+  }
+}
+
+export default ocpi;

+ 20 - 0
Strides-Admin/src/icons/svg/addon-ocpi-text.svg

@@ -0,0 +1,20 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="60.58400917053223" height="48" viewBox="0 0 60.58400917053223 48" fill="none">
+<g >
+<g filter="url(#filter_0_3)">
+<path     fill="#000000"  d="M11.896 16.036Q14.296 16.036 16.12 16.852Q18.256 17.836 19.12 20.14Q19.792 21.94 19.792 24.364Q19.792 26.812 18.976 28.612Q17.944 30.748 15.808 31.612Q14.224 32.236 12.16 32.236Q9.448 32.236 7.456 31.228Q5.776 30.388 4.888 28.588Q4 26.788 4 24.22Q4 19.636 6.28 17.692Q8.2 16.06 11.896 16.036ZM11.92 18.724Q9.568 18.724 8.56 20.26Q7.744 21.46 7.744 24.124Q7.744 26.932 8.752 28.252Q9.76 29.548 11.944 29.548Q14.056 29.548 15.052 28.228Q16.048 26.908 16.048 24.148Q16.048 21.628 15.232 20.308Q14.296 18.748 11.92 18.724ZM34.36 29.284L34.36 32.14L28.768 32.14Q27.184 32.14 26.344 31.972Q23.392 31.396 22.144 28.684Q21.328 26.86 21.328 24.172Q21.328 20.956 22.576 18.94Q23.488 17.476 25.012 16.804Q26.536 16.132 28.936 16.132L34.36 16.132L34.36 18.988L29.32 18.988Q27.04 18.988 25.984 20.212Q25.096 21.22 25.096 23.86Q25.096 27.412 26.464 28.516Q27.424 29.284 29.464 29.284L34.36 29.284ZM40.144 26.548L40.144 32.14L36.736 32.14L36.736 16.132L44.728 16.132Q46.912 16.132 47.848 16.444Q49.432 16.972 50.344 18.628Q51.016 19.876 51.016 21.34Q51.016 22.564 50.572 23.656Q50.128 24.748 49.336 25.444Q48.616 26.068 47.812 26.308Q47.008 26.548 45.616 26.548L40.144 26.548ZM40.144 23.692L44.776 23.692Q46.024 23.692 46.528 23.308Q47.272 22.708 47.272 21.22Q47.272 19.612 46.144 19.156Q45.736 18.988 44.848 18.988L40.144 18.988L40.144 23.692ZM53.176 32.14L53.176 16.132L56.584 16.132L56.584 32.14L53.176 32.14Z">
+</path>
+</g>
+</g>
+<defs>
+<filter id="filter_0_3" x="0" y="14.035995483398438" width="60.58400917053223" height="24.199996948242188" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="feFloodId_0_3"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha_0_3"/>
+<feOffset dx="0" dy="2"/>
+<feGaussianBlur stdDeviation="2"/>
+<feComposite in2="hardAlpha_0_3" operator="out"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
+<feBlend mode="normal" in2="feFloodId_0_3" result="dropShadow_1_0_3"/>
+<feBlend mode="normal" in="SourceGraphic" in2="dropShadow_1_0_3" result="shape_0_3"/>
+</filter>
+</defs>
+</svg>

+ 22 - 0
Strides-Admin/src/icons/svg/addon-ocpi.svg

@@ -0,0 +1,22 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="51.82000255584717" height="49" viewBox="0 0 51.82000255584717 49" fill="none">
+<path   fill="rgba(0, 0, 0, 1)"  d="M26.36 0C39.891 0 50.86 10.969 50.86 24.5C50.86 38.031 39.891 49 26.36 49C12.829 49 1.86 38.031 1.86 24.5Q1.86 14.3518 9.03588 7.17588Q16.2118 0 26.36 -1.90735e-06L26.36 0ZM26.36 0.999998Q16.626 1 9.74299 7.88299Q2.86 14.766 2.86 24.5C2.86 37.4787 13.3813 48 26.36 48C39.3387 48 49.86 37.4787 49.86 24.5C49.86 11.5213 39.3387 1 26.36 1L26.36 0.999998Z">
+</path>
+<g >
+<g filter="url(#filter_0_3)">
+<path     fill="#000000"  d="M10.58 17.78Q12.58 17.78 14.1 18.46Q15.88 19.28 16.6 21.2Q17.16 22.7 17.16 24.72Q17.16 26.76 16.48 28.26Q15.62 30.04 13.84 30.76Q12.52 31.28 10.8 31.28Q8.54 31.28 6.88 30.44Q5.48 29.74 4.74 28.24Q4 26.74 4 24.6Q4 20.78 5.9 19.16Q7.5 17.8 10.58 17.78ZM10.6 20.02Q8.64 20.02 7.8 21.3Q7.12 22.3 7.12 24.52Q7.12 26.86 7.96 27.96Q8.8 29.04 10.62 29.04Q12.38 29.04 13.21 27.94Q14.04 26.84 14.04 24.54Q14.04 22.44 13.36 21.34Q12.58 20.04 10.6 20.02ZM29.3 28.82L29.3 31.2L24.64 31.2Q23.32 31.2 22.62 31.06Q20.16 30.58 19.12 28.32Q18.44 26.8 18.44 24.56Q18.44 21.88 19.48 20.2Q20.24 18.98 21.51 18.42Q22.78 17.86 24.78 17.86L29.3 17.86L29.3 20.24L25.1 20.24Q23.2 20.24 22.32 21.26Q21.58 22.1 21.58 24.3Q21.58 27.26 22.72 28.18Q23.52 28.82 25.22 28.82L29.3 28.82ZM34.12 26.54L34.12 31.2L31.28 31.2L31.28 17.86L37.94 17.86Q39.76 17.86 40.54 18.12Q41.86 18.56 42.62 19.94Q43.18 20.98 43.18 22.2Q43.18 23.22 42.81 24.13Q42.44 25.04 41.78 25.62Q41.18 26.14 40.51 26.34Q39.84 26.54 38.68 26.54L34.12 26.54ZM34.12 24.16L37.98 24.16Q39.02 24.16 39.44 23.84Q40.06 23.34 40.06 22.1Q40.06 20.76 39.12 20.38Q38.78 20.24 38.04 20.24L34.12 20.24L34.12 24.16ZM44.98 31.2L44.98 17.86L47.82 17.86L47.82 31.2L44.98 31.2Z">
+</path>
+</g>
+</g>
+<defs>
+<filter id="filter_0_3" x="0" y="15.779998779296875" width="51.82000255584717" height="21.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="feFloodId_0_3"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha_0_3"/>
+<feOffset dx="0" dy="2"/>
+<feGaussianBlur stdDeviation="2"/>
+<feComposite in2="hardAlpha_0_3" operator="out"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
+<feBlend mode="normal" in2="feFloodId_0_3" result="dropShadow_1_0_3"/>
+<feBlend mode="normal" in="SourceGraphic" in2="dropShadow_1_0_3" result="shape_0_3"/>
+</filter>
+</defs>
+</svg>

+ 90 - 0
Strides-Admin/src/router/addition/OcpiRouter.js

@@ -0,0 +1,90 @@
+import Layout from '@/layout'
+
+export default {
+  path: '/ocpi',
+  redirect: 'noRedirect',
+  component: Layout,
+  alwaysShow: true,
+  meta: {
+    title: 'OCPI',
+    icon: 'addon-ocpi-text',
+    activeIcon: 'addon-ocpi',
+    affix: true
+  },
+  children: [{
+    path: '/ocpi/roaming-operators',
+    component: () => import('@/views/ocpi/operators'),
+    name: 'ocpi-roaming-operators',
+    meta: {
+      title: "Roaming Operators",
+      icon: 'sidebar-submenu-item',
+      activeIcon: 'sidebar-submenu-item-active',
+      breadcrumb: true
+    }
+  },{
+    path: '/ocpi/send-config',
+    component: () => import('@/views/ocpi/configs'),
+    name: 'ocpi-send-config',
+    meta: {
+      title: "Send Config",
+      icon: 'sidebar-submenu-item',
+      activeIcon: 'sidebar-submenu-item-active',
+      breadcrumb: true
+    }
+  },{
+    path: '/ocpi/roaming-contracts',
+    component: () => import('@/views/ocpi/contracts'),
+    name: 'ocpi-roaming-contracts',
+    meta: {
+      title: "Roaming Contracts",
+      icon: 'sidebar-submenu-item',
+      activeIcon: 'sidebar-submenu-item-active',
+      breadcrumb: true
+    }
+  },{
+    path: '/ocpi/sites',
+    component: () => import('@/views/ocpi/sites'),
+    name: 'ocpi-sites',
+    meta: {
+      title: "Sites",
+      icon: 'sidebar-submenu-item',
+      activeIcon: 'sidebar-submenu-item-active',
+      breadcrumb: true
+    }
+  },{
+    path: '/ocpi/stations',
+    component: () => import('@/views/ocpi/stations'),
+    name: 'ocpi-stations',
+    meta: {
+      title: "Charging Stations",
+      icon: 'sidebar-submenu-item',
+      activeIcon: 'sidebar-submenu-item-active',
+      breadcrumb: true
+    }
+  },{
+    path: '/ocpi/connectors',
+    component: () => import('@/views/ocpi/connectors'),
+    name: 'ocpi-connectors',
+    meta: {
+      title: "Connectors",
+      icon: 'sidebar-submenu-item',
+      activeIcon: 'sidebar-submenu-item-active',
+      breadcrumb: true
+    }
+  },{
+    path: '/ocpi/edit-operators/:id',
+    component: () => import('@/views/ocpi/views/DetailOperators'),
+    name: 'ocpi-device-index',
+    meta: {
+      title: "Edit Partner",
+      icon: 'sidebar-submenu-item',
+      activeIcon: 'sidebar-submenu-item-active',
+      activeMenu: '/ocpi/roaming-operators',
+      breadcrumb: true,
+      parent: {
+        title: 'Roaming Partner',
+        path: "/ocpi/roaming-operators"
+      }
+    }
+  }]
+}

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

@@ -1,7 +1,9 @@
+import OcpiRouter from './OcpiRouter'
 import PosRouter from './PosRouter'
 import RfidRouter from './RfidRouter'
 
 export default [
   PosRouter,
-  RfidRouter
+  RfidRouter,
+  OcpiRouter
 ]

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

@@ -91,7 +91,6 @@ const constantRoutes = [
   PartnershipRouter,
   AccessRouter,
   ...additionalRoute,
-  /*OCPI*/
   SettingsRouter
 ]
 

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

@@ -73,24 +73,24 @@
         label="Action"
         align="center"
         min-width="70">
-          <template slot-scope="{row}">
-            <!-- <TableAction
-              :showDel="false"
-              :showView="true"
-              :showEdit="false"
-              @view="viewNotification(row)"/> -->
-            <el-dropdown
-              class="action-dropdown"
-              @command="(v) => handleCommand(v, row)">
-              <i class="el-icon-more icon-action"></i>
-              <el-dropdown-menu slot="dropdown">
-                <el-dropdown-item
-                  command="viewNotification">
-                  View
-                </el-dropdown-item>
-              </el-dropdown-menu>
-            </el-dropdown>
-          </template>
+        <template slot-scope="{row}">
+          <!-- <TableAction
+            :showDel="false"
+            :showView="true"
+            :showEdit="false"
+            @view="viewNotification(row)"/> -->
+          <el-dropdown
+            class="action-dropdown"
+            @command="(v) => handleCommand(v, row)">
+            <i class="el-icon-more icon-action"></i>
+            <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item
+                command="viewNotification">
+                View
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+        </template>
       </el-table-column>
     </el-table>
     <div class="right">

+ 230 - 0
Strides-Admin/src/views/ocpi/configs.vue

@@ -0,0 +1,230 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <div class="filter-view">
+        <el-select
+          class="filter-view-item"
+          v-model="filters.pageCriteria.countryCode"
+          placeholder="Country"
+          @change="onSearch">
+          <el-option 
+            v-for="(item, index) in options.country"
+            :key="index"
+            :value="item.countryCode"
+            :label="item.countryName"/>
+        </el-select>
+        <el-input
+          class="filter-view-item"
+          v-model="filters.pageCriteria.partyName"
+          placeholder="Roaming Operator Name"
+          prefix-icon="el-icon-search"
+          @change="onSearch"
+          clearable/>
+        <div
+          class="filter-flex-button"
+          v-if="!$route.meta.onlyView">
+          <el-button
+            type="primary"
+            @click="onClickSend(true)">
+            SEND CREDENTIALS
+          </el-button>
+        </div>
+      </div>
+    </div>
+    <el-table
+      v-loading="table.loading"
+      :data="table.list">
+      <el-table-column
+        align="center"
+        label="Remarks"
+        prop="remarks"
+        min-width="180"/>
+      <el-table-column
+        align="center"
+        label="Address"
+        prop="address"
+        min-width="150"/>
+      <el-table-column
+        align="center"
+        label="Token"
+        prop="token"
+        min-width="150"/>
+      <el-table-column
+        align="center"
+        label="Date Created"
+        prop="createTime"
+        min-width="150"/>
+      <el-table-column
+        align="center"
+        label="Status"
+        prop="status"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Action"
+        min-width="70"
+        v-if="!$route.meta.onlyView">
+        <template v-slot="{ row }">
+          <el-dropdown
+            class="action-dropdown"
+            @command="(v) => handleCommand(v, row)"
+            v-if="row.dataStatus != 'Inactive'">
+            <i class="el-icon-more icon-action"></i>
+            <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item
+              <el-dropdown-item
+                command="onClickEdit">
+                Edit Info
+              </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>
+    <DialogConfigs
+      v-bind="action"
+      @hide="a => onClickSend(false, a)"/>
+  </div>
+</template>
+
+<script>
+import api from '../../api/dashboard';
+import ocpi from '../../http/api/ocpi.js';
+import setting from '../../settings.js';
+import Pagination from '@/components/Pagination'
+import DialogConfigs from './dialog/DialogConfigs'
+export default {
+  data() {
+    return {
+      table: {
+        list: [],
+        total: 0,
+        loading: false
+      },
+      filters: {
+        pageNum: 1,
+        pageSize: 10,
+        pageCriteria: {
+          countryCode: setting.defaultCountry,
+          partyName: ""
+        }
+      },
+      options: {
+        country: []
+      },
+      action: {
+        visible: false,
+        info: {},
+        isEdit: false
+      }
+    };
+  },
+  components: { Pagination, DialogConfigs },
+  created() {
+    this.getContryOptions();
+    this.onSearch();
+  },
+  methods: {
+    getContryOptions() {
+      api.getCountries().then(res => {
+        this.loading = false
+        if (res.data) {
+          this.options.country = res.data
+          if (this.filters.countryCode) {
+            this.getProvider();
+          }
+        }
+      }).catch(err => {
+        this.loading = false
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      })
+    },
+    onSearch() {
+      this.filters.pageNum = 1;
+      this.getTableData();
+    },
+    getTableData() {
+      this.table.loading = true;
+      ocpi.pagesSendConfig(this.filters).then(res => {
+        if (res.data.records && res.data.totalRow) {
+          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;
+      })
+    },
+    onClickSend(state, info) {
+      this.action.visible = state;
+      this.action.isEdit = false;
+      this.action.info = {};
+      if (state && info) {
+        this.action.info = info;
+        this.action.isEdit = true;
+      } else if (info) {
+        this.onSearch();
+      }
+    },
+    handleCommand(cb, item) {
+      this[cb](item)
+    },
+    onClickEdit(row) {
+      this.onClickSend(true, row)
+    },
+    onClickDelete(row) {
+      this.$confirm('Are you sure you want to delete this config?', 'Delete', {
+        confirmButtonText: 'OK',
+        cancelButtonText: 'Cancel',
+        type: 'warning'
+      }).then(res => {
+        this.deleteOperator(row.credentialTokenId);
+      })
+    },
+    deleteOperator(id) {
+      this.table.loading = true;
+      ocpi.deleteSendConfig(id).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Delete success."
+        })
+        this.getTableData()
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        })
+        this.table.loading = false;
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 25 - 0
Strides-Admin/src/views/ocpi/connectors.vue

@@ -0,0 +1,25 @@
+<template>
+  <div>
+    
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      
+    };
+  },
+  created() {
+    
+  },
+  methods: {
+    
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 25 - 0
Strides-Admin/src/views/ocpi/contracts.vue

@@ -0,0 +1,25 @@
+<template>
+  <div>
+    
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      
+    };
+  },
+  created() {
+    
+  },
+  methods: {
+    
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 366 - 0
Strides-Admin/src/views/ocpi/dialog/DialogAssignment.vue

@@ -0,0 +1,366 @@
+<template>
+  <el-dialog
+    :title="title"
+    :visible="visible"
+    :before-close="onHide"
+    custom-class="assign-dialog">
+    <div class="filter-container filter-view">
+      <el-select
+        style="min-width: 70px; max-width: 120px;"
+        clearable
+        v-model="filter.pageCriteria.status"
+        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 Site Name or Service Provider"
+          @keyup.enter.native="onSearch"/>
+      </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">
+        <el-table-column
+          align="center"
+          label="Location"
+          prop="name"
+          min-width="120"/>
+        <el-table-column
+          align="center"
+          label="Address"
+          prop="address"
+          min-width="150"/>
+        <el-table-column
+          align="center"
+          label="CHARGING STATION"
+          prop="stations"
+          min-width="120"/>
+        <el-table-column
+          align="center"
+          label="Assignment Status"
+          prop="status"
+          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.pageNum"
+        :limit.sync="filter.pageSize"
+        @pagination="getTableData"/>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import ocpi from '@/http/api/ocpi'
+import apiBase from '../../../api/apiBase.js'
+import Pagination from '@/components/Pagination'
+export default {
+  name: "DialogAssignment",
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    title: {
+      type: String,
+      default: "ASSIGN SITES"
+    },
+    isOutBound: {
+      type: Boolean,
+      default: false
+    },
+    operator: {
+      type: String|Number,
+      default: 0
+    },
+  },
+  components: {Pagination},
+  data() {
+    return {
+      filter: {
+        pageSize: 10,
+        pageNum: 1,
+        pageCriteria: {
+          status: "",
+          criteria: "",
+          credentialId: ""
+        }
+      },
+      table: {
+        data: [],
+        total: 0,
+        loading: false
+      },
+      loading: {
+        assign: false,
+        unassign: false
+      },
+      statusOptions: [],
+      selectRow: []
+    };
+  },
+  watch: {
+    visible: {
+      handler(n, o) {
+        console.log("watch.visible", n, o);
+        if (n) {
+          if (this.operator) {
+            this.filter.pageCriteria.criteria = "";
+            this.filter.pageCriteria.status = "";
+            this.filter.pageCriteria.credentialId = this.operator;
+            this.onSearch()
+          }
+        }
+      }
+    }
+  },
+  mounted() {
+    this.getStatusOptions();
+  },
+  methods: {
+    onHide() {
+      this.$emit("hide");
+    },
+    onSearch() {
+      this.filter.pageNum = 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.isOutBound 
+        ? ocpi.pagesSharePartyLocations(this.filter)
+        : ocpi.pagesPartyShareLocations(this.filter)
+      promise.then(res => {
+        if (res.data.records && res.data.totalRow) {
+          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 => {
+        ids.push(item.locationId)
+      })
+      return ids;
+    },
+    onClickAssign() {
+      const params = {
+        credentialId: this.operator,
+        locationIds: this.getSelectIds()
+      }
+      if (this.isOutBound) {
+        this.assignShareParty(params);
+      } else {
+        this.assignPartyShare(params);
+      }
+    },
+    onClickUnassign() {
+      const params = {
+        credentialId: this.operator,
+        locationIds: this.getSelectIds()
+      }
+      if (this.isOutBound) {
+        this.unassignShareParty(params);
+      } else {
+        this.unassignPartyShare(params);
+      }
+    },
+    assignShareParty(params) {
+      this.loading.assign = true;
+      ocpi.assignSharePartyLocations(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;
+      })
+    },
+    assignPartyShare(params) {
+      this.loading.assign = true;
+      ocpi.assignPartyShareLocations(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;
+      })
+    },
+    unassignShareParty(params) {
+      this.loading.unassign = true;
+      ocpi.unassignSharePartyLocations(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;
+      })
+    },
+    unassignPartyShare(params) {
+      ocpi.unassignPartyShareLocations(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>
+>>> .assign-dialog {
+  width: 65vw;
+  height: 90vh;
+  display: flex;
+  max-width: 1200px;
+  flex-direction: column;
+  margin-top: 5vh !important;
+}
+>>> .assign-dialog .el-dialog__header {
+  padding: 20px 20px 0;
+  font-weight: bold;
+}
+>>> .assign-dialog .el-dialog__body {
+  flex: 1;
+  padding: 20px;
+  display: flex;
+  overflow: hidden;
+  flex-direction: column;
+}
+.assign-table-actions {
+  display: flex;
+  padding-top: 5px;
+  flex-wrap: wrap-reverse;
+  align-items: center;
+  justify-content: flex-end;
+}
+.assign-dialog .table-view {
+  flex: 1;
+  overflow-y: auto;
+  padding-top: 10px;
+  margin-bottom: -10px;
+}
+@media screen and (max-width: 1200px) {
+  >>> .assign-dialog {
+    width: 70vw;
+  }
+}
+@media screen and (max-width: 1000px) {
+  >>> .assign-dialog {
+    width: 80vw;
+  }
+}
+@media screen and (max-width: 800px) {
+  >>> .assign-dialog {
+    width: 90vw;
+  }
+}
+@media screen and (max-width: 700px) {
+  >>> .assign-dialog {
+    width: 99vw;
+  }
+}
+@media screen and (max-width: 320px) {
+  >>> .assign-dialog {
+    width: 100%;
+    min-width: 300px;
+  }
+}
+</style>

+ 264 - 0
Strides-Admin/src/views/ocpi/dialog/DialogConfigs.vue

@@ -0,0 +1,264 @@
+<template>
+  <el-dialog
+    class="dialog-ocpi"
+    :visible="visible"
+    :before-close="e => hideDialog(false)"
+    :close-on-click-modal="false"
+    :title="(isEdit ? 'EDIT' : 'CREATE') + ' OCPI CONNECTION'">
+    <el-form
+      ref="ocpiForm"
+      :model="form"
+      :rules="rules"
+      label-width="120px"
+      label-position="right"
+      v-loading="initial">
+      <el-form-item
+        prop="url"
+        class="form-item"
+        label="Address:">
+        <el-input
+          v-model="form.address"
+          class="flex-item"
+          readonly/>
+      </el-form-item>
+      <el-form-item
+        prop="token"
+        class="form-item"
+        label="Token:">
+        <el-input
+          v-model="form.token"
+          class="flex-item"
+          readonly>
+          <el-button
+            slot="append"
+            icon="el-icon-refresh"
+            :loading="loading.token"
+            title="Refresh New Redemption Code"
+            @click="getConfigTokens"/>
+        </el-input>
+      </el-form-item>
+      <el-form-item
+        prop="remarks"
+        class="form-item"
+        label="Remarks:">
+        <el-input
+          v-model="form.remarks"
+          class="flex-item"
+          maxlength="200"/>
+      </el-form-item>
+      <div class="flexcc" style="padding-top: 20px;">
+        <el-button
+          class="button"
+          @click="hideDialog(false)">
+          CANCEL
+        </el-button>
+        <el-button
+          class="button"
+          type="primary"
+          :loading="loading.save"
+          @click="onFormSave">
+          <span style="padding: 0 10px;">SAVE</span>
+        </el-button>
+      </div>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script>
+import ocpi from '../../../http/api/ocpi.js';
+export default {
+  name: "DialogConfigs",
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    isEdit: {
+      type: Boolean,
+      default: false
+    },
+    info: {
+      type: Object,
+      default: () => {}
+    }
+  },
+  data() {
+    return {
+      initial: false,
+      loading: {
+        token: false,
+        save: false
+      },
+      form: {
+        credentialTokenId: "",
+        address: "",
+        token: "",
+        remarks: ""
+      },
+      rules: {
+        remarks: {
+          required: true,
+          message: "Remarks is required",
+          trigger: "blur"
+        }
+      }
+    };
+  },
+  watch: {
+    visible(n, o) {
+      if (n) {
+        if (this.info.credentialTokenId) {
+          this.form = this.info;
+          this.isEdit = true;
+        } else {
+          this.isEdit = false;
+          this.initial = true;
+          this.getConfigAddress();
+          this.getConfigTokens();
+        }
+      }
+    }
+  },
+  mounted() {
+    
+  },
+  methods: {
+    hideDialog(success) {
+      this.init();
+      this.$emit("hide", success || false);
+    },
+    init() {
+      this.form = {
+        credentialTokenId: "",
+        token: "",
+        remarks: ""
+      }
+      this.isEdit = false;
+      this.loading.save = false;
+      this.$nextTick(() => {
+        if (this.$refs['ocpiForm']) {
+          this.$refs['ocpiForm'].clearValidate();
+        }
+      })
+    },
+    getConfigAddress() {
+      ocpi.getSendConfigUrls().then(res => {
+        this.initial = false;
+        if (res.data) {
+          this.form.address = res.data
+        }
+      }).catch(err => {
+        this.initial = false;
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      })
+    },
+    getConfigTokens() {
+      this.loading.token = true;
+      ocpi.getSendConfigTokens().then(res => {
+        this.loading.token = false;
+        if (res.data) {
+          this.form.token = res.data
+        }
+      }).catch(err => {
+        this.loading.token = false;
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      })
+    },
+    onFormSave() {
+      this.$refs['ocpiForm'].validate((valid) => {
+        if (valid) {
+          this.isEdit ? this.onUpdate() : this.onCreate();
+        }
+      })
+    },
+    onCreate() {
+      this.loading.save = true;
+      ocpi.saveSendConfig(this.form).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Send successfully"
+        });
+        this.hideDialog(true);
+      }).catch(err => {
+        this.loading.save = false;
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      })
+    },
+    onUpdate() {
+      this.loading.save = true;
+      ocpi.updateSendConfig(this.form).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Update successfully"
+        });
+        this.hideDialog(true);
+      }).catch(err => {
+        this.loading.save = false;
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dialog-ocpi {
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  justify-content: center;
+}
+.dialog-ocpi >>> .el-dialog {
+  width: 100%;
+  max-width: 600px;
+  margin-top: 0 !important;
+}
+.dialog-ocpi >>> .el-dialog__body {
+  padding: 10px 20px 30px;
+}
+.dialog-ocpi >>> .el-form {
+  padding-right: 10px;
+}
+.dialog-ocpi >>> .el-input-group__append {
+  width: auto;
+  height: 40px;
+  text-align: center;
+  line-height: 40px;
+}
+.form-row {
+  display: flex;
+  flex-wrap: wrap;
+  padding-bottom: 5px;
+}
+.form-item {
+  flex: 1;
+  margin-left: 10px;
+  /*margin-bottom: 10px; TOP*/
+}
+.form-item >>> label {
+  /*padding: 0; TOP*/
+}
+.flex-item {
+  width: 100%;
+  min-width: 200px;
+  max-width: 500px;
+}
+@media screen and (max-width: 500px) {
+  .flex-item {
+    width: 100%;
+    max-width: none;
+  }
+}
+</style>

+ 172 - 0
Strides-Admin/src/views/ocpi/dialog/DialogOperators.vue

@@ -0,0 +1,172 @@
+<template>
+  <el-dialog
+    class="dialog-ocpi"
+    :visible="visible"
+    :before-close="e => hideDialog(false)"
+    :close-on-click-modal="false"
+    title="CREATE OCPI CONNECTION">
+    <el-form
+      ref="ocpiForm"
+      :model="form"
+      :rules="rules"
+      label-width="120px"
+      label-position="right"
+      v-loading="initial">
+      <el-form-item
+        prop="url"
+        class="form-item"
+        label="Party URL:">
+        <el-input
+          v-model="form.url"
+          class="flex-item"
+          maxlength="100"/>
+      </el-form-item>
+      <el-form-item
+        prop="token"
+        class="form-item"
+        label="Party Token:">
+        <el-input
+          v-model="form.token"
+          class="flex-item"
+          maxlength="50"/>
+      </el-form-item>
+      <div class="flexcc" style="padding-top: 20px;">
+        <el-button
+          class="button"
+          @click="hideDialog(false)">
+          CANCEL
+        </el-button>
+        <el-button
+          class="button"
+          type="primary"
+          :loading="loading"
+          @click="onFormSave">
+          INITIALIZE
+        </el-button>
+      </div>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script>
+import ocpi from '../../../http/api/ocpi.js';
+export default {
+  name: "DialogOperators",
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      initial: false,
+      loading: false,
+      form: {
+        url: "",
+        token: ""
+      },
+      rules: {
+        url: {
+          required: true,
+          message: "Url is required",
+          trigger: "blur"
+        },
+        token: {
+          required: true,
+          message: "Token is required",
+          trigger: "blur"
+        }
+      }
+    };
+  },
+  mounted() {
+    
+  },
+  methods: {
+    hideDialog(success) {
+      this.init();
+      this.$emit("hide", success || false);
+    },
+    init() {
+      this.form = {
+        url: "",
+        token: ""
+      }
+      this.loading = false;
+      this.$nextTick(() => {
+        if (this.$refs['ocpiForm']) {
+          this.$refs['ocpiForm'].clearValidate();
+        }
+      })
+    },
+    onFormSave() {
+      this.$refs['ocpiForm'].validate((valid) => {
+        if (valid) {
+          this.onReceive();
+        }
+      })
+    },
+    onReceive() {
+      this.loading = true;
+      ocpi.initRoamingOperators(this.form).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Initialize successfully"
+        });
+        this.hideDialog(true);
+      }).catch(err => {
+        this.loading = false;
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      });
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dialog-ocpi {
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  justify-content: center;
+}
+.dialog-ocpi >>> .el-dialog {
+  width: 100%;
+  max-width: 500px;
+  margin-top: 0 !important;
+}
+.dialog-ocpi >>> .el-dialog__body {
+  padding: 10px 20px 30px;
+}
+.dialog-ocpi >>> .el-form {
+  padding-right: 10px;
+}
+.form-row {
+  display: flex;
+  flex-wrap: wrap;
+  padding-bottom: 5px;
+}
+.form-item {
+  flex: 1;
+  margin-left: 10px;
+  /*margin-bottom: 10px; TOP*/
+}
+.form-item >>> label {
+  /*padding: 0; TOP*/
+}
+.flex-item {
+  width: 100%;
+  min-width: 200px;
+  max-width: 270px;
+}
+@media screen and (max-width: 500px) {
+  .flex-item {
+    width: 100%;
+    max-width: none;
+  }
+}
+</style>

+ 257 - 0
Strides-Admin/src/views/ocpi/operators.vue

@@ -0,0 +1,257 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <div class="filter-view">
+        <el-select
+          class="filter-view-item"
+          v-model="filters.pageCriteria.countryCode"
+          placeholder="Country"
+          @change="onSearch">
+          <el-option 
+            v-for="(item, index) in options.country"
+            :key="index"
+            :value="item.countryCode"
+            :label="item.countryName"/>
+        </el-select>
+        <el-input
+          class="filter-view-item"
+          v-model="filters.pageCriteria.partyName"
+          placeholder="Roaming Operator Name"
+          prefix-icon="el-icon-search"
+          @change="onSearch"
+          clearable/>
+        <div
+          class="filter-flex-button"
+          v-if="!$route.meta.onlyView">
+          <el-button
+            type="primary"
+            @click="onClickReceive(true)">
+            RECEIVE CREDENTIALS
+          </el-button>
+        </div>
+      </div>
+    </div>
+    <el-table
+      v-loading="table.loading"
+      :data="table.list">
+      <el-table-column
+        align="center"
+        label="Country Code"
+        prop="countryCode"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Party ID"
+        prop="partyId"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Name"
+        prop="partyName"
+        min-width="100"/>
+      <el-table-column
+        align="center"
+        label="Website"
+        prop="website"
+        min-width="150"/>
+      <el-table-column
+        align="center"
+        label="Logo"
+        prop="logo"
+        min-width="120">
+        <template v-slot="{row}">
+          <img
+            class="img-logo"
+            :src="row.logo"/>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="Sites"
+        prop="countOperation.sites"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Charging Stations"
+        prop="countOperation.stations"
+        min-width="150"/>
+      <el-table-column
+        align="center"
+        label="Connectors"
+        prop="countOperation.connectors"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Date Created"
+        prop="createTime"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Status"
+        prop="dataStatus"
+        min-width="120"/>
+      <el-table-column
+        align="center"
+        label="Action"
+        min-width="70"
+        v-if="!$route.meta.onlyView">
+        <template v-slot="{ row }">
+          <el-dropdown
+            class="action-dropdown"
+            @command="(v) => handleCommand(v, row)"
+            v-if="row.dataStatus != 'Inactive'">
+            <i class="el-icon-more icon-action"></i>
+            <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item
+              <el-dropdown-item
+                command="onClickEdit">
+                Edit Info
+              </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>
+    <ReceiveCredents
+      :visible="showReceive"
+      @hide="a => onClickReceive(false, a)"/>
+  </div>
+</template>
+
+<script>
+import api from '../../api/dashboard';
+import ocpi from '../../http/api/ocpi.js';
+import setting from '../../settings.js';
+import Pagination from '@/components/Pagination'
+import ReceiveCredents from './dialog/DialogOperators'
+export default {
+  data() {
+    return {
+      table: {
+        list: [],
+        total: 0,
+        loading: false
+      },
+      filters: {
+        pageNum: 1,
+        pageSize: 10,
+        pageCriteria: {
+          countryCode: setting.defaultCountry,
+          partyName: ""
+        }
+      },
+      options: {
+        country: []
+      },
+      showReceive: false
+    };
+  },
+  components: { Pagination, ReceiveCredents },
+  created() {
+    this.getContryOptions();
+    this.onSearch();
+  },
+  methods: {
+    getContryOptions() {
+      api.getCountries().then(res => {
+        this.loading = false
+        if (res.data) {
+          this.options.country = res.data
+          if (this.filters.countryCode) {
+            this.getProvider();
+          }
+        }
+      }).catch(err => {
+        this.loading = false
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      })
+    },
+    onSearch() {
+      this.filters.pageNum = 1;
+      this.getTableData();
+    },
+    getTableData() {
+      this.table.loading = true;
+      ocpi.pagesRoamingOperator(this.filters).then(res => {
+        if (res.data.records && res.data.totalRow) {
+          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;
+      })
+    },
+    onClickReceive(state, a) {
+      this.showReceive = state
+      if (a) {
+        this.onSearch();
+      }
+    },
+    handleCommand(cb, item) {
+      this[cb](item)
+    },
+    onClickEdit(row) {
+      this.$router.push({
+        path: "/ocpi/edit-operators/" + row.credentialId
+      })
+    },
+    onClickDelete(row) {
+      this.$confirm('Are you sure you want to delete this operator?', 'Delete', {
+        confirmButtonText: 'OK',
+        cancelButtonText: 'Cancel',
+        type: 'warning'
+      }).then(res => {
+        this.deleteOperator(row.credentialId);
+      })
+    },
+    deleteOperator(id) {
+      this.table.loading = true;
+      ocpi.deleteRoamingOperatorInfo(id).then(res => {
+        this.$message({
+          type: 'success',
+          message: "Delete success."
+        })
+        this.getTableData()
+      }).catch(err => {
+        this.$message({
+          type: 'error',
+          message: err
+        })
+        this.table.loading = true;
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.img-logo {
+  height: 40px;
+  object-fit: contain;
+}
+</style>

+ 25 - 0
Strides-Admin/src/views/ocpi/sites.vue

@@ -0,0 +1,25 @@
+<template>
+  <div>
+    
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      
+    };
+  },
+  created() {
+    
+  },
+  methods: {
+    
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 25 - 0
Strides-Admin/src/views/ocpi/stations.vue

@@ -0,0 +1,25 @@
+<template>
+  <div>
+    
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      
+    };
+  },
+  created() {
+    
+  },
+  methods: {
+    
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 274 - 0
Strides-Admin/src/views/ocpi/views/DetailOperators.vue

@@ -0,0 +1,274 @@
+<template>
+  <div class="container" v-loading="loading">
+    <el-form
+      :model="form"
+      :rules="rules"
+      ref="form"
+      label-position="right"
+      label-width="100px">
+      <div class="flexr">
+        <div class="content flex1">
+          <div class="section-title">OCPI INFORMATION</div>
+          <el-form-item
+            label="Name:">
+            <div class="info-text">{{form.name}}</div>
+          </el-form-item>
+          <el-form-item
+            label="Role:">
+            <div class="info-text">{{form.role}}</div>
+          </el-form-item>
+          <el-form-item
+            label="Party ID:">
+            <div class="info-text">{{form.partyId}}</div>
+          </el-form-item>
+          <el-form-item
+            label="Country:">
+            <div class="info-text">{{form.country}}</div>
+          </el-form-item>
+          <el-form-item
+            label="Website:">
+            <a target="_blank"
+              class="link-detail"
+              :href="form.website"
+              v-if="form.website">{{form.website}}</a>
+          </el-form-item>
+          <el-form-item
+            label="Logo:"
+            v-if="form.logo">
+            <img
+              class="img-logo"
+              :src="form.logo"/>
+          </el-form-item>
+        </div>
+        <div class="content flex1">
+          <div class="section-title">OCPI DATA</div>
+          <el-form-item
+            label="No. Of Sites:"
+            label-width="150px">
+            <div class="info-text">{{form.countOperation.sites}}</div>
+          </el-form-item>
+          <el-form-item
+            label="No. Of Chargers:"
+            label-width="150px">
+            <div class="info-text">{{form.countOperation.stations}}</div>
+          </el-form-item>
+          <el-form-item
+            label="No. Of Connectors:"
+            label-width="150px">
+            <div class="info-text">{{form.countOperation.connectors}}</div>
+          </el-form-item>
+          <el-form-item
+            label=""
+            label-width="30px">
+            <div class="status-row" v-for="(item, index) in Object.keys(form.countConnectorStatus)" :key="index">
+              <div class="status-icon">●</div>
+              <div class="status-name">{{item}}: </div>
+              <div>{{form.countConnectorStatus[item]}}</div>
+            </div>
+          </el-form-item>
+        </div>
+      </div>
+      <div class="flexr">
+        <div class="content flex1">
+          <div class="section-title">CONFIGURE SITES FROM PARTNER TO SHARE ON APP</div>
+          <el-form-item
+            label="Sites:"
+            label-width="100px">
+            <div class="info-text">
+              <span class="link-type" @click="showAssignment(false)">{{form.countShareOnApp.assigned}}/{{form.countShareOnApp.locations}}</span>
+            </div>
+          </el-form-item>
+        </div>
+        <div class="content flex1">
+          <div class="section-title">CONFIGURE SITE TO SHARE TO PARTNER</div>
+          <el-form-item
+            label="Sites:"
+            label-width="100px">
+            <div class="info-text">
+              <span class="link-type" @click="showAssignment(true)">{{form.countShareToPartner.assigned}}/{{form.countShareToPartner.locations}}</span>
+            </div>
+          </el-form-item>
+        </div>
+      </div>
+      <div class="content flexcr">
+        <div class="buttons">
+          <el-button
+            @click="onBack"
+            type="primary"
+            class="cancel-button">
+            Back
+          </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>
+    <DialogAssignment
+      :visible="assign.visible"
+      :title="assign.title"
+      :operator="form.credentialId"
+      :is-out-bound="assign.isOutBound"
+      @hide="hideAssignment"/>
+  </div>
+</template>
+
+<script>
+import ocpi from '../../../http/api/ocpi.js';
+import DialogAssignment from '../dialog/DialogAssignment';
+export default {
+  data() {
+    return {
+      loading: false,
+      form: {
+        credentialId: "",
+        name: "",
+        role: "",
+        partyId: "",
+        countryCode: "",
+        country: "",
+        website: "",
+        logo: "",
+        countOperation: {
+          sites: 0,
+          stations: 0,
+          connectors: 0
+        },
+        countConnectorStatus: {},
+        countShareOnApp: {
+          assigned: 0,
+          locations: 0
+        },
+        countShareToPartner: {
+          assigned: 0,
+          locations: 0
+        }
+      },
+      rules: {
+        
+      },
+      assign: {
+        visible: false,
+        title: "",
+        isOutBound: false
+      }
+    };
+  },
+  components: {DialogAssignment},
+  created() {
+    if (this.$route.params.id) {
+      this.getOperatorInfo()
+    }
+  },
+  methods: {
+    onBack() {
+      this.$nextTick(() => {
+        this.$router.replace({
+          path: "/ocpi/roaming-operators"
+        })
+      })
+    },
+    getOperatorInfo() {
+      this.loading = true;
+      ocpi.getRoamingOperatorInfo(this.$route.params.id).then(res => {
+        this.loading = false;
+        if (res.data) {
+          this.form = res.data
+        }
+      }).catch(err => {
+        this.loading = false;
+        this.$message({
+          type: 'error',
+          message: err
+        })
+      })
+    },
+    showAssignment(isOut) {
+      let title = isOut ? "SHARING OUTBOUND" : "SHARING INBOUND";
+      title += " (" + this.form.name + ")";
+      this.assign.title = title;
+      this.assign.isOutBound = isOut;
+      this.assign.visible = true;
+    },
+    hideAssignment() {
+      this.assign.visible = 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;
+  }
+  .info-text {
+    color: #000;
+    font-size: 14px;
+    span {
+      line-height: 20px;
+    }
+  }
+  .img-logo {
+    height: 80px;
+    max-width: 150px;
+    object-fit: contain;
+  }
+  .status-row {
+    color: #000;
+    display: flex;
+    font-size: 14px;
+    line-height: 1.8;
+    align-items: center;
+  }
+  .status-icon {
+    height:22px;
+    font-size: 12px;
+    line-height:20px;
+  }
+  .status-name {
+    padding-left: 5px;
+    padding-right: 10px;
+    text-transform: uppercase;
+  }
+  .buttons {
+    padding-top: 15px;
+    padding-bottom: 15px;
+  }
+  ::v-deep .el-form-item {
+    margin-bottom: 0;
+  }
+  @media screen and (max-width: 500px) {
+    .container {
+      padding: 0px;
+    }
+    .content {
+      padding: 15px 30px;
+    }
+  }
+</style>