mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 11:37:08 +00:00
514 lines
14 KiB
Vue
514 lines
14 KiB
Vue
<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>
|