mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 08:07:08 +00:00
497 lines
14 KiB
Vue
497 lines
14 KiB
Vue
<template>
|
||
<v-container fluid>
|
||
<v-card>
|
||
<v-card-title>
|
||
<v-toolbar flat>
|
||
<v-toolbar-title>Preplots</v-toolbar-title>
|
||
<v-spacer></v-spacer>
|
||
<v-text-field
|
||
v-model="filter"
|
||
append-icon="mdi-magnify"
|
||
label="Filter"
|
||
single-line
|
||
clearable
|
||
hint="Filter by line number, first or last shotpoint or remarks. Use ‘incr’ or ‘+’ / ‘decr’ or ‘-’ to show only incrementing / decrementing lines"
|
||
></v-text-field>
|
||
</v-toolbar>
|
||
</v-card-title>
|
||
<v-card-text>
|
||
|
||
<v-menu v-if="writeaccess()"
|
||
v-model="contextMenuShow"
|
||
:position-x="contextMenuX"
|
||
:position-y="contextMenuY"
|
||
absolute
|
||
offset-y
|
||
>
|
||
<v-list dense v-if="contextMenuItem">
|
||
<template v-if="!selectOn">
|
||
<v-list-item @click="setNTBA" v-if="contextMenuItem.ntba || (contextMenuItem.num_points == contextMenuItem.na)">
|
||
<v-list-item-title v-if="contextMenuItem.ntba"
|
||
title="Mark the line as part of the acquisition plan"
|
||
>Unset NTBA</v-list-item-title>
|
||
<v-list-item-title v-else
|
||
title="Mark the line as not to be acquired"
|
||
>Set NTBA</v-list-item-title>
|
||
</v-list-item>
|
||
<v-list-item @click="setComplete" v-if="contextMenuItem.na && (contextMenuItem.num_points != contextMenuItem.na || contextMenuItem.tba != contextMenuItem.na)">
|
||
<v-list-item-title v-if="contextMenuItem.tba != contextMenuItem.na"
|
||
title="Mark any remaining points as pending acquisition"
|
||
>Unset line complete</v-list-item-title>
|
||
<v-list-item-title v-else
|
||
title="Mark any remaining points as not to be acquired"
|
||
>Set line complete</v-list-item-title>
|
||
</v-list-item>
|
||
<v-list-item @click="addToPlan" v-if="!contextMenuItem.ntba && !isPlanned(contextMenuItem)">
|
||
<v-list-item-title>Add to plan</v-list-item-title>
|
||
</v-list-item>
|
||
<v-list-item @click="removeFromPlan" v-if="isPlanned(contextMenuItem)">
|
||
<v-list-item-title>Remove from plan</v-list-item-title>
|
||
</v-list-item>
|
||
<v-list-item @click="showLineColourDialog">
|
||
<v-list-item-title>Set colour…</v-list-item-title>
|
||
</v-list-item>
|
||
</template>
|
||
<template v-else>
|
||
<v-list-item @click="showLineColourDialog">
|
||
<v-list-item-title>Set colour…</v-list-item-title>
|
||
</v-list-item>
|
||
</template>
|
||
<v-divider></v-divider>
|
||
<v-list-item>
|
||
<v-list-item-action-text>Multi-select</v-list-item-action-text>
|
||
<v-list-item-action><v-checkbox v-model="selectOn"></v-checkbox></v-list-item-action>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-menu>
|
||
|
||
<v-dialog
|
||
v-model="colourPickerShow"
|
||
max-width="300"
|
||
>
|
||
<v-card>
|
||
<v-card-title>Choose line colour</v-card-title>
|
||
<v-card-text>
|
||
<v-color-picker
|
||
v-model="selectedColour"
|
||
show-swatches
|
||
hide-canvas
|
||
hide-inputs
|
||
hide-mode-switch
|
||
@update:color="selectedColour.alpha = 0.5"
|
||
>
|
||
</v-color-picker>
|
||
</v-card-text>
|
||
<v-card-actions>
|
||
<v-btn small
|
||
@click="selectedColour=null; setLineColour()"
|
||
>
|
||
Clear
|
||
</v-btn>
|
||
<v-spacer></v-spacer>
|
||
<v-btn small
|
||
@click="setLineColour"
|
||
>
|
||
Set
|
||
</v-btn>
|
||
<v-spacer></v-spacer>
|
||
<v-btn small
|
||
@click="colourPickerShow=false"
|
||
>
|
||
Cancel
|
||
</v-btn>
|
||
</v-card-actions>
|
||
</v-card>
|
||
</v-dialog>
|
||
|
||
<v-data-table
|
||
:headers="headers"
|
||
:items="items"
|
||
:items-per-page.sync="itemsPerPage"
|
||
:server-items-length="lineCount"
|
||
item-key="line"
|
||
:search="filter"
|
||
:loading="linesLoading"
|
||
:options.sync="options"
|
||
fixed-header
|
||
:footer-props='{itemsPerPageOptions: [ 10, 25, 50, 100, 500, -1 ], showFirstLastPage: true}'
|
||
:item-class="itemClass"
|
||
:show-select="selectOn"
|
||
v-model="selectedRows"
|
||
@click:row="setActiveItem"
|
||
@contextmenu:row="contextMenu"
|
||
>
|
||
|
||
<template v-slot:item.status="{item}">
|
||
<dougal-line-status
|
||
:preplot="item"
|
||
:sequences="sequences.filter(s => s.line == item.line)"
|
||
:sequence-href="(s) => `/projects/${$route.params.project}/log/sequence/${s.sequence}`"
|
||
:planned-sequences="plannedSequences.filter(s => s.line == item.line)"
|
||
:planned-sequence-href="() => `/projects/${$route.params.project}/plan`"
|
||
:pending-reshoots="null"
|
||
:pending-reshoot-href="null"
|
||
>
|
||
<template v-slot:empty>
|
||
<div v-if="!item.ntba" class="sequence" title="Virgin"></div>
|
||
<div v-else class="sequence ntba" title="Not to be acquired"><span>NTBA</span></div>
|
||
</template>
|
||
</dougal-line-status>
|
||
</template>
|
||
|
||
<template v-slot:item.tba="{item, value}">
|
||
<span :class="!value && (item.na ? 'warning--text' : 'success--text')">{{ value }}</span>
|
||
</template>
|
||
|
||
<template v-slot:item.length="props">
|
||
<span>{{ Math.round(props.value) }} m</span>
|
||
</template>
|
||
|
||
<template v-slot:item.azimuth="props">
|
||
<span>{{ props.value.toFixed(2) }} °</span>
|
||
</template>
|
||
|
||
<template v-slot:item.remarks="{item}">
|
||
<v-text-field v-if="edit && edit.line == item.line && edit.key == 'remarks'"
|
||
type="text"
|
||
v-model="edit.value"
|
||
prepend-icon="mdi-restore"
|
||
append-outer-icon="mdi-content-save-edit-outline"
|
||
clearable
|
||
@click:prepend="edit.value = item.remarks; edit = null"
|
||
@click:append-outer="edit = null"
|
||
>
|
||
</v-text-field>
|
||
<div v-else>
|
||
<span v-html="$options.filters.markdownInline(item.remarks)"></span>
|
||
<v-btn v-if="writeaccess() && edit === null"
|
||
icon
|
||
small
|
||
title="Edit"
|
||
:disabled="linesLoading"
|
||
@click="editItem(item, 'remarks')"
|
||
>
|
||
<v-icon small>mdi-square-edit-outline</v-icon>
|
||
</v-btn>
|
||
</div>
|
||
|
||
</template>
|
||
|
||
</v-data-table>
|
||
|
||
</v-card-text>
|
||
</v-card>
|
||
</v-container>
|
||
</template>
|
||
|
||
<style lang="stylus" scoped>
|
||
.ntba
|
||
background-image repeating-linear-gradient(-45deg, #ff00004d, #ff000080 10px, #d3d3d31a 10px, #d3d3d314 20px)
|
||
cursor not-allowed
|
||
display flex
|
||
align-items center
|
||
justify-content center
|
||
</style>
|
||
|
||
<script>
|
||
import { mapActions, mapGetters } from 'vuex';
|
||
import DougalLineStatus from '@/components/line-status';
|
||
import AccessMixin from '@/mixins/access';
|
||
|
||
export default {
|
||
name: "LineList",
|
||
|
||
components: {
|
||
DougalLineStatus
|
||
},
|
||
|
||
mixins: [
|
||
AccessMixin
|
||
],
|
||
|
||
data () {
|
||
return {
|
||
headers: [
|
||
{
|
||
value: "line",
|
||
text: "Line"
|
||
},
|
||
{
|
||
value: "status",
|
||
text: "Status"
|
||
},
|
||
{
|
||
value: "fsp",
|
||
text: "FSP",
|
||
align: "end"
|
||
},
|
||
{
|
||
value: "lsp",
|
||
text: "LSP",
|
||
align: "end"
|
||
},
|
||
{
|
||
value: "num_points",
|
||
text: "Points",
|
||
align: "end"
|
||
},
|
||
{
|
||
value: "na",
|
||
text: "Virgin",
|
||
align: "end"
|
||
},
|
||
{
|
||
value: "tba",
|
||
text: "Remaining",
|
||
align: "end"
|
||
},
|
||
{
|
||
value: "length",
|
||
text: "Length",
|
||
align: "end"
|
||
},
|
||
{
|
||
value: "azimuth",
|
||
text: "Azimuth",
|
||
align: "end"
|
||
},
|
||
{
|
||
value: "remarks",
|
||
text: "Remarks"
|
||
}
|
||
],
|
||
items: [],
|
||
selectOn: false,
|
||
selectedRows: [],
|
||
filter: "",
|
||
options: {},
|
||
lineCount: null,
|
||
//sequences: [],
|
||
activeItem: null,
|
||
edit: null, // {line, key, value}
|
||
queuedReload: false,
|
||
itemsPerPage: 25,
|
||
|
||
// Context menu stuff
|
||
contextMenuShow: false,
|
||
contextMenuX: 0,
|
||
contextMenuY: 0,
|
||
contextMenuItem: null,
|
||
|
||
// Colour picker stuff
|
||
colourPickerShow: false,
|
||
selectedColour: null,
|
||
styles: null
|
||
}
|
||
},
|
||
|
||
computed: {
|
||
...mapGetters(['user', 'linesLoading', 'lines', 'sequences', 'plannedSequences'])
|
||
},
|
||
|
||
watch: {
|
||
|
||
options: {
|
||
handler () {
|
||
this.fetchLines();
|
||
},
|
||
deep: true
|
||
},
|
||
|
||
async lines () {
|
||
await this.fetchLines();
|
||
},
|
||
|
||
async edit (newVal, oldVal) {
|
||
if (newVal === null && oldVal !== null) {
|
||
const item = this.items.find(i => i.line == oldVal.line);
|
||
|
||
// Get around this Vuetify ‘feature’
|
||
// https://github.com/vuetifyjs/vuetify/issues/4144
|
||
if (oldVal.value === null) oldVal.value = "";
|
||
|
||
if (item && item[oldVal.key] != oldVal.value) {
|
||
if (await this.saveItem(oldVal)) {
|
||
item[oldVal.key] = oldVal.value;
|
||
} else {
|
||
this.edit = oldVal;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
filter (newVal, oldVal) {
|
||
if (newVal?.toLowerCase() != oldVal?.toLowerCase()) {
|
||
this.fetchLines();
|
||
}
|
||
},
|
||
|
||
itemsPerPage (newVal, oldVal) {
|
||
localStorage.setItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`, newVal);
|
||
},
|
||
|
||
user (newVal, oldVal) {
|
||
this.itemsPerPage = Number(localStorage.getItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`)) || 25;
|
||
}
|
||
|
||
},
|
||
|
||
methods: {
|
||
|
||
itemClass (item) {
|
||
|
||
const colourClass = item.meta.colour ? "bg-clr-"+item.meta.colour.slice(1) : null;
|
||
if (colourClass && ![...this.styles.cssRules].some(i => i.selectorText == "."+colourClass)) {
|
||
const rule = `.${colourClass} { background-color: ${item.meta.colour}; }`;
|
||
this.styles.insertRule(rule);
|
||
}
|
||
|
||
return [
|
||
item.meta.colour ? colourClass : "",
|
||
(this.activeItem == item && !this.edit) ? 'blue accent-1 elevation-3' : ''
|
||
];
|
||
},
|
||
|
||
isPlanned(item) {
|
||
return this.sequences.find(i => i.line == item.line && i.status == 'planned');
|
||
},
|
||
|
||
contextMenu (e, {item}) {
|
||
e.preventDefault();
|
||
this.contextMenuShow = false;
|
||
this.contextMenuX = e.clientX;
|
||
this.contextMenuY = e.clientY;
|
||
this.contextMenuItem = item;
|
||
this.$nextTick( () => this.contextMenuShow = true );
|
||
},
|
||
|
||
setNTBA () {
|
||
this.removeFromPlan();
|
||
this.saveItem({
|
||
line: this.contextMenuItem.line,
|
||
key: 'ntba',
|
||
value: !this.contextMenuItem.ntba
|
||
})
|
||
},
|
||
|
||
setComplete () {
|
||
this.saveItem({
|
||
line: this.contextMenuItem.line,
|
||
key: 'complete',
|
||
value: this.contextMenuItem.na && this.contextMenuItem.tba == this.contextMenuItem.na
|
||
})
|
||
},
|
||
|
||
async addToPlan () {
|
||
const payload = {
|
||
sequence: null,
|
||
line: this.contextMenuItem.line,
|
||
fsp: this.contextMenuItem.fsp,
|
||
lsp: this.contextMenuItem.lsp
|
||
}
|
||
const url = `/project/${this.$route.params.project}/plan`;
|
||
const init = {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: payload
|
||
}
|
||
await this.api([url, init]);
|
||
},
|
||
|
||
async removeFromPlan () {
|
||
const plannedLine = this.sequences.find(i => i.status == "planned" && i.line == this.contextMenuItem.line);
|
||
if (plannedLine && plannedLine.sequence) {
|
||
const url = `/project/${this.$route.params.project}/plan/${plannedLine.sequence}`;
|
||
const init = {
|
||
method: "DELETE"
|
||
}
|
||
await this.api([url, init]);
|
||
}
|
||
},
|
||
|
||
showLineColourDialog () {
|
||
this.selectedColour = this.contextMenuItem.meta.colour
|
||
? {hexa: this.contextMenuItem.meta.colour}
|
||
: null;
|
||
this.colourPickerShow = true;
|
||
},
|
||
|
||
setLineColour () {
|
||
const items = this.selectOn ? this.selectedRows : [ this.contextMenuItem ];
|
||
const colour = this.selectedColour ? this.selectedColour.hex+"80" : null;
|
||
|
||
this.selectedRows = [];
|
||
this.selectOn = false;
|
||
|
||
for (const item of items) {
|
||
if (colour) {
|
||
item.meta.colour = colour;
|
||
} else {
|
||
delete item.meta.colour;
|
||
}
|
||
this.saveItem({line: item.line, key: "meta", value: item.meta});
|
||
this.colourPickerShow = false;
|
||
}
|
||
},
|
||
|
||
editItem (item, key) {
|
||
this.edit = {
|
||
line: item.line,
|
||
key,
|
||
value: item[key]
|
||
}
|
||
},
|
||
|
||
async saveItem (edit) {
|
||
if (!edit) return;
|
||
|
||
try {
|
||
const url = `/project/${this.$route.params.project}/line/${edit.line}`;
|
||
const init = {
|
||
method: "PATCH",
|
||
body: {
|
||
[edit.key]: edit.value
|
||
}
|
||
};
|
||
|
||
let res;
|
||
await this.api([url, init, (e, r) => res = r]);
|
||
return res && res.ok;
|
||
} catch (err) {
|
||
return false;
|
||
}
|
||
},
|
||
|
||
setActiveItem (item) {
|
||
this.activeItem = this.activeItem == item
|
||
? null
|
||
: item;
|
||
},
|
||
|
||
|
||
async fetchLines (opts = {}) {
|
||
const options = {
|
||
text: this.filter,
|
||
...this.options
|
||
};
|
||
const res = await this.getLines([this.$route.params.project, options]);
|
||
this.items = res.lines;
|
||
this.lineCount = res.count;
|
||
},
|
||
|
||
...mapActions(["api", "getLines"])
|
||
},
|
||
|
||
mounted () {
|
||
this.fetchLines();
|
||
|
||
// Initialise stylesheet
|
||
const el = document.createElement("style");
|
||
document.head.appendChild(el);
|
||
this.styles = document.styleSheets[document.styleSheets.length-1];
|
||
}
|
||
|
||
}
|
||
|
||
</script>
|