europeum web ui

This commit is contained in:
Justinas K. 2021-12-02 20:31:03 +02:00
parent 6fafdd334b
commit f49261c2a6
17 changed files with 28228 additions and 0 deletions

23
parking-occupancy-managment/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,24 @@
# europeum
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

27495
parking-occupancy-managment/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
{
"name": "europeum",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"lodash": "^4.17.21",
"vue": "^2.6.11",
"vue-axios": "^3.4.0",
"vue-material": "^1.0.0-beta-15"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:400,500,700,400italic|Material+Icons"/>
<title>Europeum</title>
</head>
<body>
<noscript>
<strong>We're sorry but page doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -0,0 +1,59 @@
<template>
<div id="app">
<Toolbar />
<div class="md-layout">
<div class="md-layout-item md-size-10"></div>
<div class="md-layout-item"><TenantList :enabled="!notConnected" /></div>
<div class="md-layout-item md-size-10"></div>
</div>
<md-dialog :md-active="notConnected" :md-click-outside-to-close="false" :md-close-on-esc="false">
<md-dialog-title>Could not connect</md-dialog-title>
<md-dialog-content>
Retrying connection to host, please see if CredoID is up and running
</md-dialog-content>
<md-progress-bar
class="md-accent"
md-mode="indeterminate"
></md-progress-bar>
</md-dialog>
</div>
</template>
<script>
import TenantList from "./components/TenantList.vue";
import Toolbar from "./components/Toolbar.vue";
import axios from "axios";
import { BASE_URL } from "@/globals";
export default {
name: "App",
data: () => ({ notConnected: false }),
components: {
TenantList,
Toolbar,
},
methods: {
pingScriptHost() {
axios
.get(`${BASE_URL}/ping`)
.then(() => (this.notConnected = false))
.catch(() => (this.notConnected = true));
},
startAutoUpadte() {
this.timer = setInterval(this.pingScriptHost, 5000);
},
cancelAutoUpdate() {
clearInterval(this.timer);
},
},
created() {
this.pingScriptHost();
this.startAutoUpadte();
},
};
</script>
<style>
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,165 @@
<template>
<div>
<md-dialog :md-active="value">
<md-dialog-title>Settings</md-dialog-title>
<md-list>
<md-subheader>Occupancy</md-subheader>
<md-list-item>
<md-field>
<label for="accesslevel">Occupancy access Level</label>
<md-select v-model="selectedAccessLevelId" name="accesslevel">
<md-option
v-for="item in accessLevels"
v-bind:key="item.id"
:value="item.id"
>{{ item.name }}</md-option
>
</md-select>
</md-field>
</md-list-item>
<md-list-item>
<md-field>
<label for="entryReaders">Entry readers</label>
<md-select v-model="entryReaderIds" name="entryReaders" multiple>
<md-option
v-for="item in entryReaders"
v-bind:key="item.id"
:value="item.id"
>{{ item.name }}
{{ item.isEntry ? " (Entry)" : " (Exit)" }}</md-option
>
</md-select>
</md-field>
</md-list-item>
<md-list-item>
<md-field>
<label for="exitReaders">Exit readers</label>
<md-select v-model="exitReaderIds" name="exitReaders" multiple>
<md-option
v-for="item in exitReaders"
v-bind:key="item.id"
:value="item.id"
>{{ item.name }}
{{ item.isEntry ? " (Entry)" : " (Exit)" }}</md-option
>
</md-select>
</md-field>
</md-list-item>
<md-subheader>Other</md-subheader>
<md-list-item>
<md-field>
<label for="refresh">Tenant list refresh interval</label>
<md-select v-model="refreshInterval" name="refresh">
<md-option value="2">2 seconds</md-option>
<md-option value="5">5 seconds</md-option>
<md-option value="10">10 seconds</md-option>
<md-option value="60">1 minute</md-option>
</md-select>
</md-field>
</md-list-item>
<md-list-item>
<md-button
class="md-raised md-accent reset-button"
@click="confirmReset = true"
>Reset</md-button
>
</md-list-item>
</md-list>
<md-dialog-actions>
<md-button class="md-primary" @click="$emit('input', false)"
>Close</md-button
>
<md-button class="md-primary" @click="updateSettingsData()"
>Save</md-button
>
</md-dialog-actions>
</md-dialog>
<md-dialog-confirm
:md-active.sync="confirmReset"
md-title="Reset"
md-content="Reset current configuration to default"
md-confirm-text="Reset"
md-cancel-text="Cancel"
@md-cancel="confirmReset = false"
@md-confirm="onConfirmReset"
/>
</div>
</template>
<script>
import axios from "axios";
import { EventBus } from "@/event-bus";
import { BASE_URL } from "@/globals";
export default {
name: "Settings",
props: {
value: Boolean,
},
data: () => ({
accessLevels: [],
exitReaders: [],
entryReaders: [],
entryReaderIds: [],
exitReaderIds: [],
selectedAccessLevelId: 0,
refreshInterval: 5,
confirmReset: false,
}),
methods: {
fetchAccessLevelData() {
axios
.get(`${BASE_URL}/access-levels`)
.then((response) => (this.accessLevels = response.data));
},
fetchReaderData() {
axios.get(`${BASE_URL}/readers`).then((response) => {
this.entryReaders = response.data;
this.exitReaders = response.data;
});
},
updateSettingsData() {
axios.put(`${BASE_URL}/settings`, {
accessLevelId: this.selectedAccessLevelId,
entryReaderIds: this.entryReaderIds,
exitReaderIds: this.exitReaderIds,
});
localStorage.refreshInterval = this.refreshInterval;
this.$emit("input", false);
EventBus.$emit("settings-update", {
refreshInterval: this.refreshInterval,
});
},
fetchSettingsData() {
axios.get(`${BASE_URL}/settings`).then((response) => {
this.selectedAccessLevelId = response.data.accessLevelId;
this.entryReaderIds = response.data.entryReaderIds;
this.exitReaderIds = response.data.exitReaderIds;
});
this.refreshInterval = localStorage.refreshInterval ?? 5;
},
onConfirmReset() {
axios
.post(`${BASE_URL}/reset`)
.then(() => this.fetchSettingsData());
},
},
created() {
this.fetchAccessLevelData();
this.fetchSettingsData();
this.fetchReaderData();
},
};
</script>
<style>
.reset-button {
margin: 0;
}
</style>

View File

@ -0,0 +1,81 @@
<template>
<div class="md-layout text-center spinner-layout custom-layout">
<div class="md-layout-item text-center">
<md-button class="md-icon-button md-mini md-raised" @click="dec()">
<md-icon>remove</md-icon>
</md-button>
</div>
<div class="md-layout-item md-size-30">
<input :value="value" @change="inputChange" class="custom-input" @keypress="isNumber" />
<!-- <label class="md-display-1 spinner-label">{{ this.value }}</label> -->
</div>
<div class="md-layout-item">
<md-button class="md-icon-button md-mini md-raised" @click="inc()">
<md-icon>add</md-icon>
</md-button>
</div>
</div>
</template>
<script>
export default {
name: "Spinner",
props: {
value: Number,
min: Number,
max: Number,
},
data: () => ({}),
methods: {
inc() {
this.$emit("input", this.value + 1);
},
dec() {
if (this.value > 0) {
this.$emit("input", this.value - 1);
}
},
isNumber(val) {
if (isNaN(Number(val.key))) {
return val.preventDefault();
}
},
inputChange(event) {
this.$emit("input", Number(event.target.value));
//this.$emit("input-change", Number(event.target.value));
},
},
created() {
this.count = 0;
}
};
</script>
<style>
.spinner-label {
width: 1.5em;
display: inline-block;
text-align: center;
}
.custom-input {
margin: 0;
padding-top: 0;
line-height: 42px;
text-align: center;
max-width: 73px;
border: 0;
font-family: Roboto, Noto Sans, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 17px;
}
.custom-layout {
max-width: 235px;
margin: 0 auto;
}
.text-center {
text-align: center;
}
</style>

View File

@ -0,0 +1,250 @@
<template>
<div>
<md-table
md-card
v-model="tenants"
md-sort="name"
md-sort-order="asc"
md-model-id="id"
md-fixed-header
>
<md-table-toolbar>
<div class="md-toolbar-section-start">
<h1 class="md-title">Occupancy management</h1>
</div>
<div class="md-toolbar-section-end">
<md-button
class="md-icon-button md-mini md-raised"
@click="synchronizeTenantsForced()"
md-tooltip="Synchronize tenants"
>
<md-icon>sync</md-icon>
<md-tooltip md-direction="top">Synchronize tenants</md-tooltip>
</md-button>
</div>
</md-table-toolbar>
<md-table-empty-state
md-label="No tenants available"
md-description="Most likely service is offline"
>
</md-table-empty-state>
<md-table-row slot="md-table-row" slot-scope="{ item }">
<md-table-cell md-label="Name" md-sort-by="name">{{
item.name
}}</md-table-cell>
<md-table-cell md-label="Occupancy" md-sort-by="ratio">
<md-progress-bar
md-mode="determinate"
:class="{
'md-accent': item.ratio == 1,
}"
:md-value="item.ratio * 100"
></md-progress-bar>
</md-table-cell>
<md-table-cell md-label="Current">
<Spinner
v-model="item.currentOccupancy"
@input="occupancyUpdateCurrent(item)"
/>
</md-table-cell>
<md-table-cell md-label="Total capacity" md-sort-by="maxOccupancy">
<Spinner
v-model="item.maxOccupancy"
@input="occupancyUpdateMax(item)"
/>
</md-table-cell>
</md-table-row>
</md-table>
<md-snackbar
md-position="center"
:md-duration="hasError ? Infinity : 2000"
:md-active.sync="showUpdate"
md-persistent
>
<span>{{ updateMessage }}</span>
<md-button class="md-primary" @click="showUpdate = false"
>Close</md-button
>
</md-snackbar>
</div>
</template>
<script>
import axios from "axios";
import _ from "lodash";
import Spinner from "./Spinner.vue";
import { EventBus } from "@/event-bus";
import { BASE_URL } from "@/globals";
export default {
components: { Spinner },
name: "TenantList",
data: () => ({
selected: [],
showUpdate: false,
updateMessage: "",
tenants: [],
timer: "",
hasError: false,
}),
props: {
msg: String,
enabled: Boolean,
},
created() {
this.synchronizeTenants();
this.fetchTenantData();
EventBus.$once("settings-update", this.onSettingsUpdate);
},
methods: {
onSettingsUpdate() {
console.log("Settings updated");
this.startAutoUpadte();
EventBus.$once("settings-update", this.onSettingsUpdate);
},
fetchTenantData() {
axios
.get(`${BASE_URL}/tenants`)
.then((response) => {
let temp = response.data.map(function (item) {
item.ratio = item.currentOccupancy / item.maxOccupancy;
return item;
});
temp.forEach((e) => {
const found = this.tenants.find((c) => c.id == e.id);
if (found != null) {
found.name = e.name;
found.ratio = e.ratio;
found.currentOccupancy = e.currentOccupancy;
found.maxOccupancy = e.maxOccupancy;
} else {
this.tenants.push({
id: e.id,
name: e.name,
ratio: e.ratio,
currentOccupancy: e.currentOccupancy,
maxOccupancy: e.maxOccupancy,
});
}
});
const timeElapsed = Date.now();
const today = new Date(timeElapsed);
EventBus.$emit("tenant-update", today.toLocaleString());
})
.catch(() => this.showSnackbar("Status update failed"));
},
synchronizeTenantsForced() {
this.synchronizeTenants();
this.fetchTenantData();
this.tenants = [];
},
synchronizeTenants() {
axios
.post(`${BASE_URL}/sync`)
.then(() => this.showSnackbar("Tenants synchronized"))
.finally(() => this.startAutoUpadte());
},
occupancyUpdateCurrent(item) {
if (item.currentOccupancy > item.maxOccupancy) {
item.maxOccupancy = item.currentOccupancy;
}
item.ratio = item.currentOccupancy / item.maxOccupancy;
this.throttledOccupancyUpdate(item);
},
occupancyUpdateMax(item) {
if (item.currentOccupancy > item.maxOccupancy) {
item.currentOccupancy = item.maxOccupancy;
}
item.ratio = item.currentOccupancy / item.maxOccupancy;
this.throttledOccupancyUpdate(item);
},
cancelAutoUpdate() {
if (this.timer) {
clearInterval(this.timer);
console.log("Autoupdate cleared");
}
},
startAutoUpadte() {
if (this.timer) {
this.cancelAutoUpdate();
}
console.log("Autoupdate started");
this.timer = setInterval(this.fetchTenantData, this.getRefreshInterval());
},
getRefreshInterval() {
const interval = localStorage.refreshInterval != null ? localStorage.refreshInterval * 1000 : 5000;
console.log(
`Update refresh interval ${
interval
}ms`
);
return interval;
},
throttledOccupancyUpdate: _.debounce(
function (item) {
this.cancelAutoUpdate();
axios
.put(`${BASE_URL}/tenants/${item.id}/set-occupancy`, {
newOccupancyValue: Number(item.currentOccupancy),
maxOccupancyValue: Number(item.maxOccupancy),
})
.then(() => {
this.showSnackbar("Item updated");
})
.catch(() => {
this.showSnackbar("Last update failed, try reloading page", true);
})
.finally(() => this.startAutoUpadte());
},
100,
{ leading: false, trailing: true }
),
showSnackbar(message, isError = false) {
this.hasError = isError;
this.showUpdate = true;
this.updateMessage = message;
},
},
beforeDestroy() {
this.cancelAutoUpdate();
},
watch: {
enabled: function (value) {
console.log(value);
if (!value) {
this.cancelAutoUpdate();
} else {
this.startAutoUpadte();
}
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>
.md-field {
margin: 4px 0 12px;
}
.md-error {
color: rgb(233, 90, 90);
}
th:nth-child(2),
th:nth-child(3),
th:nth-child(4) {
text-align: center !important;
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<div>
<md-toolbar id="toolbar">
<md-button class="md-icon-button" @click="showSettings = true">
<md-icon>settings</md-icon>
</md-button>
<Settings v-model="showSettings" />
<div class="md-toolbar-section-end" v-if="date">
<span class="md-body-2">Last update</span><span class="md-caption">{{date}}</span>
</div>
</md-toolbar>
</div>
</template>
<script>
import Settings from "./Settings.vue";
import { EventBus } from "@/event-bus";
export default {
name: "Toolbar",
components: { Settings },
data: () => ({
showSettings: false,
date: null,
}),
created() {
EventBus.$on("tenant-update", (date) => {
this.date = date;
});
},
};
</script>
<style>
#toolbar {
margin-bottom: 24px;
}
</style>

View File

@ -0,0 +1,2 @@
import Vue from 'vue';
export const EventBus = new Vue();

View File

@ -0,0 +1,2 @@
//export const BASE_URL = 'http://localhost:58111'
export const BASE_URL = 'http://localhost:8090/scripting/europeum'

View File

@ -0,0 +1,14 @@
import Vue from 'vue'
import App from './App.vue'
import VueMaterial from 'vue-material'
import 'vue-material/dist/vue-material.min.css'
import 'vue-material/dist/theme/default.css'
Vue.config.productionTip = false
Vue.use(VueMaterial)
new Vue({
render: h => h(App),
}).$mount('#app')

View File

@ -0,0 +1,7 @@
module.exports = {
publicPath: './',
assetsDir: './',
configureWebpack: {
devtool: "source-map"
}
};