mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 13:07:08 +00:00
Implement equipment frontend component
This commit is contained in:
513
lib/www/client/source/src/views/Equipment.vue
Normal file
513
lib/www/client/source/src/views/Equipment.vue
Normal 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>
|
||||
Reference in New Issue
Block a user