Add QC view to web interface.

This commit implements most of what is required
by #23, with the exception of labels processing
as stated in https://gitlab.com/wgp/dougal/software/-/issues/23/designs/20200908_101516.jpg#note_408968681
This commit is contained in:
D. Berge
2020-09-10 23:27:53 +02:00
parent 606f1b8125
commit c82caa1d1f
3 changed files with 315 additions and 0 deletions

View File

@@ -47,6 +47,7 @@ export default {
{ href: "sequences", text: "Sequences" },
{ href: "calendar", text: "Calendar" },
{ href: "log", text: "Log" },
{ href: "qc", text: "QC" },
{ href: "map", text: "Map" }
],
path: []

View File

@@ -10,6 +10,7 @@ import SequenceList from '../views/SequenceList.vue'
import SequenceSummary from '../views/SequenceSummary.vue'
import Calendar from '../views/Calendar.vue'
import Log from '../views/Log.vue'
import QC from '../views/QC.vue'
import Map from '../views/Map.vue'
@@ -103,6 +104,10 @@ Vue.use(VueRouter)
{ path: "date/:date0/:date1", name: "logByDates" }
]
},
{
path: "qc",
component: QC
},
{
path: "map",
component: Map

View File

@@ -0,0 +1,309 @@
<template>
<v-container fluid>
<v-card>
<v-card-title>
<v-toolbar flat>
<v-toolbar-title>Project quality control</v-toolbar-title>
<v-spacer></v-spacer>
<span title="Check this to select multiple sequences">
<v-checkbox
v-model="multiple"
label="Multiple"
></v-checkbox>
</span>
<v-select
v-model="selectedSequences"
:items="multiple ? sequences : [ {text: 'All', value: null}, ...sequences ]"
style="max-width:250px;"
class="mx-2"
placeholder="Sequence"
:multiple="multiple"
>
<template v-slot:selection="{item, index}">
<v-chip small v-if="index < 3">
{{item.text || item}}
</v-chip>
<span v-if="index == 3" class="grey--text caption">(+ {{selectedSequences.length-3}} {{selectedSequences.length == 4 ? 'other' : 'others'}})</span>
</template>
</v-select>
<v-text-field
class="mx-2"
v-model="search"
append-icon="mdi-magnify"
label="Filter"
single-line
clearable
></v-text-field>
<v-checkbox
v-model="autoexpand"
label="Auto expand"
title="Automatically expand all items when searching"
></v-checkbox>
</v-toolbar>
</v-card-title>
<v-card-text>
<v-treeview
:items="filteredItems"
:open.sync="open"
item-key="serial"
:open-on-click="true"
>
<template v-slot:label="{item}">
<div @dblclick.stop.prevent="toggleChildren(item)">
{{item.name}}
<v-chip v-if="item.children && itemCount(item)"
small
color="warning"
v-text="itemCount(item)"
>
</v-chip>
</div>
</template>
</v-treeview>
</v-card-text>
</v-card>
</v-container>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
export default {
name: "QC",
data () {
return {
items: [],
open: [],
options: {},
search: null,
selectedSequences: null,
multiple: false,
autoexpand: false,
itemIndex: 0
}
},
computed: {
filteredItems () {
return this.items.map( item =>
this.filterByText(
this.filterBySequence(item, this.multiple? this.selectedSequences : this.selectedSequences && [this.selectedSequences]),
this.search
)
).filter(i => !!i);
//return this.items.map( item => this.filter(item, this.search, this.selectedSequences) ).filter(i => !!i);
},
sequences () {
function getSeq (item) {
return "_id" in item
? Array.isArray(item._id)
? Number(item._id[0])
: Number(item._id)
: "children" in item
? item.children.map(i => getSeq(i)).flat()
: undefined;
}
const s = new Set();
this.items.map(i => getSeq(i)).flat().filter(i => !!i).forEach(i => s.add(i));
return [...s].sort((a,b) => b-a); // Descending order
},
...mapGetters(['loading'])
},
watch: {
filteredItems (v) {
if (this.autoexpand) {
v.forEach( item => this.toggleChildren(item, true) );
}
},
multiple (value) {
if (value && !Array.isArray(this.selectedSequences)) {
this.selectedSequences = [];
}
}
},
methods: {
itemCount (item, count = 0) {
let sum = count;
if (item.children) {
sum += item.children.map(child => this.itemCount(child)).reduce( (a, b) => a+b, 0 )
} else {
sum++;
}
return sum;
},
filterByText(item, queryText) {
if (!queryText || !item) return item;
if (item.children) {
const newItem = Object.assign({}, item);
newItem.children = item.children.map( child => this.filterByText(child, queryText) ).filter(i => !!i)
if (newItem.children.length > 0) {
return newItem;
}
}
if (item.name && item.name.toLowerCase().indexOf(queryText.toLowerCase()) > -1) {
return item;
}
},
filterBySequence(item, sequences) {
if (!sequences || !sequences.length) return item;
if (item._id) {
if ( (item._id.length > 1 && sequences.includes(item._id[0])) || sequences.includes(item) ) {
return item;
}
}
if (item.children) {
const newItem = Object.assign({}, item);
newItem.children = item.children.map( child => this.filterBySequence(child, sequences) ).filter(i => !!i);
if (newItem.children.length > 0) {
return newItem;
}
}
},
filterXX (item, queryText, sequences) {
if (!queryText && !sequences) return item;
const number = /\d+/.test(queryText)
? Number(queryText)
: null;
const newItem = Object.assign({}, item);
if (item.children) {
newItem.children = item.children.map( child => this.filter(child, queryText, sequences) ).filter(i => !!i)
if (newItem.children.length) {
return newItem;
}
}
if (sequences) {
if (!Array.isArray(sequences)) {
sequences = [sequences];
}
if ("_id" in item) {
if ( (item._id.length > 1 && sequences.includes(item._id[0])) || (sequences.includes(item._id)) ) {
if (queryText) {
return (item.name && item.name.toLowerCase().indexOf(queryText.toLowerCase()) > -1)
? newItem
: null;
}
return newItem;
}
}
} else if (item.name && item.name.toLowerCase().indexOf(queryText.toLowerCase()) > -1) {
return newItem;
}
},
toggleChildren (item, state) {
const open = typeof state == 'undefined'
? !this.open.includes(item.serial)
: state;
if (item.children) {
item.children.forEach(child => this.toggleChildren(child, open));
}
if (open) {
if (!this.open.includes(item.serial)) {
this.open.push(item.serial);
}
} else {
const index = this.open.indexOf(item.serial);
if (index > -1) {
this.open.splice(index, 1);
}
}
},
transform (item) {
item.serial = ++this.itemIndex;
if (item.check) {
switch (item.iterate) {
case "sequences":
item.check = item.check.map(check => ({
_id: check._id,
name: `Sequence ${check._id}: ${check.results}`
}));
break;
case "shots":
default:
const bySequence = {};
for (const check of item.check) {
if (!bySequence[check._id[0]]) {
bySequence[check._id[0]] = [];
}
bySequence[check._id[0]].push({
_id: check._id,
name: `Point ${check._id[1]}: ${check.results}`
});
}
item.check = Object.keys(bySequence).map(seq => ({
_id: seq,
name: `Sequence: ${seq}`,
children: bySequence[seq]
}));
}
if (!("children" in item)) {
item.children = item.check;
delete item.check;
}
}
if (item.children) {
for (const child of item.children) {
this.transform(child);
}
if (item.check) {
item.children = item.check.concatenate(item.children);
}
}
return item;
},
async getQCData () {
const url = `/project/${this.$route.params.project}/info/qc`;
const res = await this.api([url]);
if (res) {
this.items = res.results.map(i => this.transform(i)) || [];
this.updatedOn = res.updatedOn;
} else {
this.items = [];
this.updatedOn = null;
}
},
...mapActions(["api"])
},
mounted () {
this.getQCData();
}
}
</script>