Implement equipment frontend component

This commit is contained in:
D. Berge
2021-05-20 18:32:27 +02:00
parent c832d8b107
commit fc58a4d435

View File

@@ -0,0 +1,513 @@
<template>
<v-container fluid>
<v-row>
<v-col>
<v-dialog
max-width="600px"
:value="dialog"
@input="closeDialog"
>
<template v-slot:activator="{ on, attrs }">
<v-btn v-if="writeaccess"
small
color="primary"
v-bind="attrs"
v-on="on"
>Add</v-btn>
</template>
<v-card>
<v-card-title v-if="dialogMode=='new'">Add new item</v-card-title>
<v-card-title v-else>Edit item</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12">
<v-text-field
label="Kind"
required
v-model="item.kind"
:disabled="dialogMode == 'edit'"
>
</v-text-field>
</v-col>
<v-col cols="12">
<v-textarea
class="markdown"
label="Description"
dense
auto-grow
rows="1"
v-model="item.description"
>
</v-textarea>
</v-col>
<v-col cols="6">
<v-text-field
label="Date"
type="date"
step="1"
v-model="item.date"
>
</v-text-field>
</v-col>
<v-col cols="6">
<v-text-field
label="Time"
type="time"
step="60"
v-model="item.time"
>
</v-text-field>
</v-col>
<template v-for="(attr, idx) in item.attributes">
<v-col cols="4">
<v-text-field
label="Attribute"
v-model="attr.key"
>
</v-text-field>
</v-col>
<v-col cols="8">
<v-textarea
label="Value"
class="markdown"
auto-grow
rows="1"
v-model="attr.value"
>
<template v-slot:append-outer>
<v-btn
fab
x-small
dark
color="red"
title="Remove this attribute / value pair"
@click="removeAttribute(idx)"
>
<v-icon>mdi-minus</v-icon>
</v-btn>
</template>
</v-textarea>
</v-col>
</template>
<v-col cols="12" class="text-right">
<v-btn
fab
x-small
color="primary"
title="Add a new attribute / value pair to further describe the equipment"
@click="addAttribute"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-btn
color="warning"
@click="closeDialog"
>
Cancel
</v-btn>
<v-spacer></v-spacer>
<v-btn
color="success"
:loading="loading"
:disabled="!canSave || loading"
@click="saveItem"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-col>
</v-row>
<v-row>
<v-col cols="4">
<v-toolbar
dense
flat
>
<v-toolbar-title>
Equipment
</v-toolbar-title>
</v-toolbar>
<v-list dense two-line>
<v-subheader v-if="!latest.length">
There are no items of equipment
</v-subheader>
<v-list-item-group
v-model="selectedIndex"
color="primary"
>
<v-list-item v-for="(item, idx) in latest" :key="idx">
<v-list-item-content>
<v-list-item-title>
{{item.kind}}
</v-list-item-title>
<v-list-item-subtitle>
Last updated: {{item.tstamp.substring(0,16)}}Z
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</v-col>
<v-col cols="8">
<v-card v-if="selectedItem">
<v-card-title>{{selectedItem.kind}}</v-card-title>
<v-card-subtitle class="text-caption">{{selectedItem.tstamp}}</v-card-subtitle>
<v-card-text>
<v-container>
<v-row>
<div v-html="$options.filters.markdown(selectedItem.description||'')"></div>
</v-row>
<v-row>
<v-simple-table>
<template v-slot:default>
<tbody>
<tr v-for="(attr, idx) in selectedItem.attributes" :key="idx">
<td>{{attr.key}}</td>
<td v-html="$options.filters.markdown(attr.value||'')"></td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-btn v-if="writeaccess"
small
text
color="primary"
title="Make a change to this item"
@click="editItem(selectedItem)"
>
Update
</v-btn>
<v-btn-toggle
group
v-model="historyMode"
>
<v-btn
small
text
:disabled="false"
title="View item's full history of changes"
>
History
</v-btn>
</v-btn-toggle>
<v-spacer></v-spacer>
<v-btn v-if="writeaccess"
small
dark
color="red"
title="Remove this instance from the item's history"
@click="confirmDelete(selectedItem)"
>
Delete
</v-btn>
</v-card-actions>
</v-card>
<v-subheader v-else-if="latest.length" class="justify-center">Select an item from the list</v-subheader>
<v-expand-transition v-if="selectedItem">
<div v-if="historyMode===0">
<v-subheader v-if="!selectedItemHistory || !selectedItemHistory.length"
class="justify-center"
>No more history</v-subheader>
<v-card v-for="item in selectedItemHistory" class="mt-5">
<v-card-title>{{selectedItem.kind}}</v-card-title>
<v-card-subtitle class="text-caption">{{item.tstamp}}</v-card-subtitle>
<v-card-text>
<v-container>
<v-row>
<div v-html="$options.filters.markdown(item.description||'')"></div>
</v-row>
<v-row>
<v-simple-table>
<template v-slot:default>
<tbody>
<tr v-for="(attr, idx) in item.attributes" :key="idx">
<td>{{attr.key}}</td>
<td v-html="$options.filters.markdown(attr.value||'')"></td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn v-if="writeaccess"
small
dark
color="red"
title="Remove this instance from the item's history"
@click="confirmDelete(item)"
>
Delete
</v-btn>
</v-card-actions>
</v-card>
</div>
</v-expand-transition>
</v-col>
</v-row>
<v-dialog
:value="confirm.message"
max-width="500px"
persistent
>
<v-sheet
class="px-7 pt-7 pb-4 mx-auto text-center d-inline-block"
color="blue-grey darken-3"
dark
>
<div class="grey--text text--lighten-1 text-body-2 mb-4" v-html="confirm.message"></div>
<v-btn
:disabled="loading"
class="ma-1"
color="grey"
plain
@click="cancelConfirmAction"
>
{{ confirm.no || "Cancel" }}
</v-btn>
<v-btn
:loading="loading"
class="ma-1"
color="error"
plain
@click="doConfirmAction"
>
{{ confirm.yes || "Delete" }}
</v-btn>
</v-sheet>
</v-dialog>
</v-container>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
export default {
name: "Equipment",
data () {
return {
latest: [],
all: [],
item: {
kind: null,
description: null,
tstamp: null,
date: null,
time: null,
attributes: []
},
dialogMode: null,
selectedIndex: null,
historyMode: false,
confirm: {
message: null,
action: null,
yes: null,
no: null
}
}
},
watch: {
dialog (newVal, oldVal) {
if (newVal) {
const tstamp = new Date();
this.item.date = tstamp.toISOString().substr(0, 10);
this.item.time = tstamp.toISOString().substr(11, 5);
}
},
"item.date": function (newVal) {
if (newVal) {
this.item.tstamp = new Date(this.item.date+"T"+this.item.time);
}
},
"item.time": function (newVal) {
if (newVal) {
this.item.tstamp = new Date(this.item.date+"T"+this.item.time);
}
},
async serverEvent (event) {
if (event.payload.schema == "public") {
if (event.channel == "info") {
if (!this.loading) {
this.getEquipment();
}
}
}
}
},
computed: {
dialog () {
return !!this.dialogMode;
},
canSave () {
return this.item.kind &&
this.item.date && this.item.time &&
(this.item.attributes.length
? this.item.attributes.every(i => i.key && i.value)
: (this.item.description ||"").trim());
},
selectedItem () {
return this.selectedIndex !== null
? this.latest[this.selectedIndex]
: null;
},
selectedItemHistory () {
if (this.selectedItem && this.historyMode === 0) {
const items = this.all
.filter(i => i.kind == this.selectedItem.kind && i.tstamp != this.selectedItem.tstamp)
.sort( (a, b) => new Date(b.tstamp) - new Date(a.tstamp) );
return items;
}
return null;
},
...mapGetters(['user', 'writeaccess', 'loading', 'serverEvent'])
},
methods: {
async cancelConfirmAction () {
this.confirm.action = null;
this.confirm.message = null;
this.confirm.yes = null;
this.confirm.no = null;
},
async doConfirmAction () {
await this.confirm.action();
this.cancelConfirmAction();
},
async getEquipment () {
const url = `/info/equipment`;
const items = await this.api([url]) || [];
this.all = [...items];
this.latest = this.all.filter(i =>
!this.all.find(j => i.kind == j.kind && i.tstamp < j.tstamp)
)
.sort( (a, b) => a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0 );
},
addAttribute () {
this.item.attributes.push({key: undefined, value: undefined});
},
removeAttribute (idx) {
this.item.attributes.splice(idx, 1);
},
async deleteItem (item) {
const idx = this.all.findIndex(i => i.kind == item.kind && i.tstamp == item.tstamp);
if (idx == -1) {
return;
}
const url = `/info/equipment/${idx}`;
const init = {
method: "DELETE"
};
await this.api([url, init]);
await this.getEquipment();
},
confirmDelete (item) {
this.confirm.action = () => this.deleteItem(item);
this.confirm.message = "Are you sure? <b>This action is irreversible.</b>";
},
clearItem () {
this.item.kind = null;
this.item.description = null;
this.item.date = null;
this.item.time = null;
this.item.attributes = [];
},
editItem (item) {
this.item.kind = item.kind;
this.item.description = item.description;
this.item.tstamp = new Date();
this.item.attributes = [...item.attributes];
this.dialogMode = "edit";
this.dialog = true;
},
async saveItem () {
const item = {};
item.kind = this.item.kind;
item.description = this.item.description;
item.tstamp = this.item.tstamp.toISOString();
item.attributes = [...this.item.attributes.filter(i => i.key && i.value)];
if (this.dialogMode == "edit") {
this.latest.splice(this.selectedIndex, 1, item);
} else {
this.latest.push(item);
}
const url = `/info/equipment`;
const init = {
method: "POST",
body: item
};
await this.api([url, init]);
this.closeDialog();
await this.getEquipment();
},
clearItem () {
this.item.kind = null;
this.item.description = null;
this.item.attributes = [];
this.item.tstamp = null;
},
closeDialog (state = false) {
this.clearItem();
this.dialogMode = state===true ? "new" : null;
},
...mapActions(["api"])
},
async mounted () {
await this.getEquipment();
}
}
</script>