This commit is contained in:
j3d1 2024-11-24 21:58:26 +01:00
parent c056aafe50
commit 985e7cef08
13 changed files with 417 additions and 372 deletions

View file

@ -42,9 +42,8 @@ export default {
url: this.src,
data: this.image_data
});
}
},
mounted() {
deferImage() {
setTimeout(() => {
if (this.cached) {
const c = this.getThumbnail(this.src);
@ -56,5 +55,14 @@ export default {
this.loadImage();
}, 0);
}
},
watch: {
src: function (newVal, oldVal) {
this.deferImage()
}
},
mounted() {
this.deferImage();
}
}
</script>

View file

@ -1,222 +0,0 @@
<template>
<ol class="timeline">
<li v-for="(item, index) in timeline" :key="index"
:class="{'timeline-item':true, 'extra-space': item.type === 'mail'}">
<span class="timeline-item-icon filled-icon" v-if="item.type === 'mail'">
<font-awesome-icon icon="envelope"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'comment'">
<font-awesome-icon icon="comment"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'state'"
:class="'bg-' + stateInfo(item.state).color">
<font-awesome-icon :icon="stateInfo(item.state).icon"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'assignment'" :class="'bg-secondary'">
<font-awesome-icon icon="user"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'item_relation'">
<font-awesome-icon icon="object-group"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'shipping_voucher'">
<font-awesome-icon icon="truck"/>
</span>
<span class="timeline-item-icon faded-icon" v-else>
<font-awesome-icon icon="pen"/>
</span>
<TimelineMail v-if="item.type === 'mail'" :item="item"/>
<TimelineComment v-else-if="item.type === 'comment'" :item="item"/>
<TimelineStateChange v-else-if="item.type === 'state'" :item="item"/>
<TimelineAssignment v-else-if="item.type === 'assignment'" :item="item"/>
<TimelineRelatedItem v-else-if="item.type === 'item_relation'" :item="item"/>
<TimelineShippingVoucher v-else-if="item.type === 'shipping_voucher'" :item="item"/>
<p v-else>{{ item }}</p>
</li>
<li class="timeline-item">
<span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="comment"/>
</span>
<div class="new-comment card bg-dark">
<div class="">
<textarea placeholder="add comment..." v-model="newComment" class="form-control">
</textarea>
<AsyncButton class="btn btn-primary float-right" :task="addCommentAndClear">
<font-awesome-icon icon="comment"/>
Save Comment
</AsyncButton>
</div>
</div>
</li>
</ol>
</template>
<script>
import TimelineMail from "@/components/TimelineMail.vue";
import TimelineComment from "@/components/TimelineComment.vue";
import TimelineStateChange from "@/components/TimelineStateChange.vue";
import {mapActions, mapGetters} from "vuex";
import TimelineAssignment from "@/components/TimelineAssignment.vue";
import TimelineRelatedItem from "@/components/TimelineRelatedItem.vue";
import TimelineShippingVoucher from "@/components/TimelineShippingVoucher.vue";
import AsyncButton from "@/components/inputs/AsyncButton.vue";
export default {
name: 'Timeline',
components: {
TimelineShippingVoucher, AsyncButton,
TimelineRelatedItem, TimelineAssignment, TimelineStateChange, TimelineComment, TimelineMail
},
props: {
timeline: {
type: Array,
default: () => []
}
},
emits: ['sendMail', 'addComment'],
data: () => ({
newMail: "",
newComment: ""
}),
computed: {
...mapGetters(['stateInfo']),
newestMailSubject() {
const mail = this.timeline.filter(item => item.type === 'mail').pop();
return mail ? mail.subject : "";
},
},
methods: {
...mapActions(['sendMail', 'postComment']),
sendMailAndClear: async function () {
await this.sendMail(this.newMail);
this.newMail = "";
},
addCommentAndClear: async function () {
await this.postComment(this.newComment);
this.newComment = "";
}
}
};
</script>
<style lang="scss" scoped>
*,
*:before,
*:after {
box-sizing: border-box;
}
button,
input,
select,
textarea {
font: inherit;
}
a {
color: inherit;
}
img {
display: block;
max-width: 100%;
}
/* End basic CSS override */
.timeline {
width: 85%;
display: flex;
flex-direction: column;
padding: 32px 0 32px 32px;
border-left: 2px solid var(--gray);
font-size: 1.125rem;
margin: 0 auto;
}
.timeline-item {
display: flex;
gap: 24px;
& + * {
margin-top: 24px;
}
& + .extra-space {
margin-top: 48px;
}
}
.new-comment, .new-mail {
width: 100%;
textarea, input {
border: 1px solid var(--gray);
border-radius: 6px;
height: 5em;
padding: 8px 16px;
&::placeholder {
color: var(--gray-dark);
}
&:focus {
border-color: var(--gray-dark);
outline: 0; /* Don't actually do this */
box-shadow: 0 0 0 4px var(--dark);
}
}
}
.timeline-item-icon {
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 50%;
margin-left: -57px;
flex-shrink: 0;
overflow: hidden;
svg {
width: 20px;
height: 20px;
}
&.faded-icon {
background-color: var(--secondary);
color: var(--light);
}
&.filled-icon {
background-color: var(--primary);
color: var(--light);
}
}
.button {
border: 0;
display: inline-flex;
vertical-align: middle;
margin-right: 4px;
margin-top: 12px;
align-items: center;
justify-content: center;
font-size: 1rem;
height: 32px;
padding: 0 8px;
background-color: var(--dark);
flex-shrink: 0;
cursor: pointer;
border-radius: 99em;
&:hover {
background-color: var(--gray);
}
}
</style>

View file

@ -21,6 +21,9 @@
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'shipping_voucher'">
<font-awesome-icon icon="truck"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'placement'">
<font-awesome-icon icon="archive"/>
</span>
<span class="timeline-item-icon faded-icon" v-else>
<font-awesome-icon icon="pen"/>
</span>
@ -30,40 +33,15 @@
<TimelineAssignment v-else-if="item.type === 'assignment'" :item="item"/>
<TimelineRelatedItem v-else-if="item.type === 'item_relation'" :item="item"/>
<TimelineShippingVoucher v-else-if="item.type === 'shipping_voucher'" :item="item"/>
<TimelinePlacement v-else-if="item.type === 'placement'" :item="item"/>
<TimelineRelatedTicket v-else-if="item.type === 'issue_relation'" :item="item"/>
<p v-else>{{ item }}</p>
</li>
<li class="timeline-item">
<span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="comment"/>
</span>
<div class="new-comment card bg-dark">
<div class="">
<textarea placeholder="add comment..." v-model="newComment" class="form-control">
</textarea>
<AsyncButton class="btn btn-primary float-right" :task="addCommentAndClear">
<font-awesome-icon icon="comment"/>
Save Comment
</AsyncButton>
</div>
</div>
<slot name="timeline_action1"/>
</li>
<li class="timeline-item">
<span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="envelope"/>
</span>
<div class="new-mail card bg-dark">
<div class="card-header">
{{ newestMailSubject }}
</div>
<div>
<textarea placeholder="reply mail..." v-model="newMail" class="form-control">
</textarea>
<AsyncButton class="btn btn-primary float-right" :task="sendMailAndClear">
<font-awesome-icon icon="envelope"/>
Send Mail
</AsyncButton>
</div>
</div>
<slot name="timeline_action2"/>
</li>
</ol>
</template>
@ -78,12 +56,20 @@ import TimelineAssignment from "@/components/TimelineAssignment.vue";
import TimelineRelatedItem from "@/components/TimelineRelatedItem.vue";
import TimelineShippingVoucher from "@/components/TimelineShippingVoucher.vue";
import AsyncButton from "@/components/inputs/AsyncButton.vue";
import TimelinePlacement from "@/components/TimelinePlacement.vue";
import TimelineRelatedTicket from "@/components/TimelineRelatedTicket.vue";
export default {
name: 'Timeline',
components: {
TimelineShippingVoucher, AsyncButton,
TimelineRelatedItem, TimelineAssignment, TimelineStateChange, TimelineComment, TimelineMail
TimelineRelatedTicket,
TimelinePlacement,
TimelineShippingVoucher,
TimelineRelatedItem,
TimelineAssignment,
TimelineStateChange,
TimelineComment,
TimelineMail
},
props: {
timeline: {
@ -91,33 +77,13 @@ export default {
default: () => []
}
},
emits: ['sendMail', 'addComment'],
data: () => ({
newMail: "",
newComment: ""
}),
computed: {
...mapGetters(['stateInfo']),
newestMailSubject() {
const mail = this.timeline.filter(item => item.type === 'mail').pop();
return mail ? mail.subject : "";
...mapGetters(['stateInfo'])
},
},
methods: {
...mapActions(['sendMail', 'postComment']),
sendMailAndClear: async function () {
await this.sendMail(this.newMail);
this.newMail = "";
},
addCommentAndClear: async function () {
await this.postComment(this.newComment);
this.newComment = "";
}
}
};
</script>
<style lang="scss" scoped>
<style lang="scss">
*,
*:before,
@ -136,10 +102,10 @@ a {
color: inherit;
}
img {
/*img {
display: block;
max-width: 100%;
}
}*/
/* End basic CSS override */

View file

@ -0,0 +1,85 @@
<template>
<div class="timeline-item-description">
<i class="avatar | small">
<font-awesome-icon icon="user"/>
</i>
<span><a href="#">$USER</a> has placed item in '{{item.box}}' (#{{item.cid}}) at <time
:datetime="timestamp">{{ timestamp }}</time>
</span>
</div>
</template>
<script>
export default {
name: 'TimelinePlacement',
props: {
'item': {
type: Object,
required: true
}
},
computed: {
'timestamp': function () {
return new Date(this.item.timestamp).toLocaleString();
},
}
};
</script>
<style scoped>
a {
color: inherit;
}
/* End basic CSS override */
.timeline-item-description {
display: flex;
padding-top: 6px;
gap: 8px;
color: var(--gray);
img {
flex-shrink: 0;
}
a {
/*color: var(--c-grey-500);*/
font-weight: 500;
text-decoration: none;
&:hover,
&:focus {
outline: 0; /* Don't actually do this */
color: var(--info);
}
}
}
.avatar {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
overflow: hidden;
aspect-ratio: 1 / 1;
flex-shrink: 0;
width: 40px;
height: 40px;
&.small {
width: 28px;
height: 28px;
}
img {
object-fit: cover;
}
}
</style>

View file

@ -5,8 +5,12 @@
<i class="avatar | small">
<font-awesome-icon icon="user"/>
</i>
<span><!--a href="#">$USER</a--> linked item <span class="badge badge-secondary">#{{ item.item.uid }} </span> on <time
:datetime="timestamp">{{ timestamp }}</time> as <span class="badge badge-primary">{{ item.status }}</span>
<span><!--a href="#">$USER</a--> linked item <span class="badge badge-secondary">#{{
item.item.id
}} </span> on <time
:datetime="timestamp">{{ timestamp }}</time> as <span class="badge badge-primary">{{
item.status
}}</span>
</span>
</div>
<div class="card bg-dark">
@ -20,8 +24,10 @@
</div>
<div class="col">
<div class="card-body">
<h6 class="card-subtitle text-secondary">uid: {{ item.item.uid }} box: {{ item.item.box }}</h6>
<h6 class="card-subtitle text-secondary">id: {{ item.item.id }} box: {{ item.item.box }}</h6>
<router-link :to="{name: 'item', params: {id: item.item.id}}">
<h6 class="card-title">{{ item.item.description }}</h6>
</router-link>
<!--div class="row mx-auto mt-2">
<div class="btn-group">
<button class="btn btn-outline-success"

View file

@ -0,0 +1,98 @@
<template>
<div class="timeline-item-wrapper">
<div class="timeline-item-description">
<i class="avatar | small">
<font-awesome-icon icon="user"/>
</i>
<span> linked ticket <span class="badge badge-secondary">#{{ item.issue_thread.id }} </span> on
<time :datetime="timestamp">{{ timestamp }}</time> as
<span class="badge badge-primary">{{ item.status }}</span>
</span>
</div>
<div class="timeline-item-description">
<router-link :to="{name: 'ticket', params: {id: item.issue_thread.id}}">
<h6 class="card-title">Ticket #{{ item.issue_thread.id }} - {{ item.issue_thread.name }}</h6>
</router-link>
</div>
</div>
</template>
<script>
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue";
import Lightbox from "@/components/Lightbox.vue";
export default {
name: 'TimelineRelatedTicket',
components: {Lightbox},
props: {
'item': {
type: Object,
required: true
}
},
computed: {
'timestamp': function () {
return new Date(this.item.timestamp).toLocaleString();
}
},
};
</script>
<style scoped>
a {
color: inherit;
}
.timeline-item-description {
display: flex;
padding-top: 6px;
gap: 8px;
color: var(--gray);
img {
flex-shrink: 0;
}
a {
/*color: var(--c-grey-500);*/
font-weight: 500;
text-decoration: none;
&:hover,
&:focus {
outline: 0; /* Don't actually do this */
color: var(--info);
}
}
}
.card {
border: 1px solid var(--gray);
}
.avatar {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
overflow: hidden;
aspect-ratio: 1 / 1;
flex-shrink: 0;
width: 40px;
height: 40px;
&.small {
width: 28px;
height: 28px;
}
img {
object-fit: cover;
}
}
</style>

View file

@ -1,6 +1,6 @@
import {createApp} from 'vue'
import App from './App.vue';
import VueQrcode from '@chenfengyuan/vue-qrcode';
//import VueQrcode from '@chenfengyuan/vue-qrcode';
import store from './store';
import router from './router';
@ -53,6 +53,6 @@ library.add(faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, fa
const app = createApp(App).use(store).use(router);
app.component(VueQrcode.name, VueQrcode);
//app.component(VueQrcode.name, VueQrcode);
app.component('font-awesome-icon', FontAwesomeIcon);
app.mount('#app')

View file

@ -159,7 +159,7 @@ const store = createStore({
},
updateItem(state, updatedItem) {
const item = state.loadedItems[updatedItem.event ? updatedItem.event : 'none'].filter(
({uid}) => uid === updatedItem.uid)[0];
({id}) => id === updatedItem.id)[0];
Object.assign(item, updatedItem);
},
removeItem(state, item) {
@ -352,14 +352,13 @@ const store = createStore({
const {data, success} = await getters.session.delete(`/2/events/${event_id}/`);
if (success) {
await dispatch('loadEvents')
commit('replaceEvents', [...state.events.filter(e => e.eid !== event_id)])
commit('replaceEvents', [...state.events.filter(e => e.id !== event_id)])
}
},
async updateEvent({commit, dispatch, state}, {id, partial_event}){
console.log(id, partial_event);
const {data, success} = await http.patch(`/2/events/${id}/`, partial_event, state.user.token);
if (success) {
commit('replaceEvents', [...state.events.filter(e => e.eid !== id), data])
commit('replaceEvents', [...state.events.filter(e => e.id !== id), data])
}
},
async fetchTicketStates({commit, state, getters}) {
@ -421,16 +420,16 @@ const store = createStore({
async updateItem({commit, getters, state}, item) {
const {
data, success
} = await getters.session.put(`/2/${getters.getEventSlug}/item/${item.uid}/`, item);
} = await getters.session.put(`/2/${getters.getEventSlug}/item/${item.id}/`, item);
commit('updateItem', data);
},
async markItemReturned({commit, getters, state}, item) {
await getters.session.patch(`/2/${getters.getEventSlug}/item/${item.uid}/`, {returned: true},
await getters.session.patch(`/2/${getters.getEventSlug}/item/${item.id}/`, {returned: true},
state.user.token);
commit('removeItem', item);
},
async deleteItem({commit, getters, state}, item) {
await getters.session.delete(`/2/${getters.getEventSlug}/item/${item.uid}/`, item);
await getters.session.delete(`/2/${getters.getEventSlug}/item/${item.id}/`, item);
commit('removeItem', item);
},
async postItem({commit, getters, state}, item) {

View file

@ -2,15 +2,47 @@
<AsyncLoader :loaded="!!item.id">
<div class="container-fluid px-xl-5 mt-3">
<div class="row">
<div class="col-xl-8 offset-xl-2">
<div class="col-lg-3 col-xl-2">
<div class="card bg-dark text-light mb-2" id="filters">
<div class="card bg-dark">
<AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`"
class="d-block card-img"
@click="openLightboxModalWith(item)"
/>
<div class="card-body">
<h6 class="card-subtitle text-secondary">id: {{ item.id }} box: {{
item.box
}}</h6>
<h6 class="card-title">{{ item.description }}</h6>
</div>
</div>
</div>
</div>
<div class="col-xl-8">
<div class="card bg-dark text-light mb-2" id="filters">
<div class="card-header">
<h3>Item #{{ item.id }} - {{ item.description }}</h3>
</div>
<div>
{{ item }}
<Timeline :timeline="item.timeline">
<template v-slot:timeline_action1>
<span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="comment"/>
</span>
<div class="new-comment card bg-dark">
<div class="">
<textarea placeholder="add comment..." v-model="newComment"
class="form-control">
</textarea>
<AsyncButton class="btn btn-primary float-right" :task="addCommentAndClear">
<font-awesome-icon icon="comment"/>
Save Comment
</AsyncButton>
</div>
<ItemTimeline :timeline="item.timeline" @sendMail="handleMail" @addComment="handleComment"/>
</div>
</template>
</Timeline>
<div class="card-footer d-flex justify-content-between">
<button class="btn btn-secondary mr-2" @click="$router.go(-1)">Back</button>
<div class="btn-group">
@ -53,6 +85,21 @@
</div>
</div>
</div>
<div class="col-lg-3 col-xl-2" v-if="item.related_issues && item.related_issues.length">
<div class="card bg-dark text-light mb-2" id="filters">
<div class="card-body">
<h5 class="card-title text-info">Related</h5>
</div>
<div class="card bg-dark" v-for="issue in item.related_issues" v-bind:key="item.id">
<div class="card-body">
<router-link :to="{name: 'ticket', params: {id: issue.id}}">
<h6 class="card-title">Ticket #{{ issue.id }} - {{ issue.name }}</h6>
</router-link>
<h6 class="card-subtitle text-secondary">state: {{ issue.state }}</h6>
</div>
</div>
</div>
</div>
</div>
</div>
</AsyncLoader>
@ -60,20 +107,19 @@
<script>
import {mapActions, mapGetters, mapState} from 'vuex';
import ItemTimeline from "@/components/ItemTimeline.vue";
import Timeline from "@/components/Timeline.vue";
import ClipboardButton from "@/components/inputs/ClipboardButton.vue";
import AsyncLoader from "@/components/AsyncLoader.vue";
import InputCombo from "@/components/inputs/InputCombo.vue";
import InputString from "@/components/inputs/InputString.vue";
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
export default {
name: 'Item',
components: {InputString, InputCombo, AsyncLoader, ClipboardButton, ItemTimeline},
components: {AuthenticatedImage, InputString, InputCombo, AsyncLoader, ClipboardButton, Timeline},
data() {
return {
selected_state: null,
selected_assignee: null,
shipping_voucher_type: null,
newComment: ""
}
},
computed: {
@ -90,21 +136,9 @@ export default {
}
},
methods: {
...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']),
...mapActions(['deleteItem', 'markItemReturned', 'updateTicketPartial', 'postComment']),
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']),
...mapActions(['claimShippingVoucher', 'fetchShippingVouchers', 'loadEventItems', 'loadBoxes']),
handleMail(mail) {
this.sendMail({
id: this.item.id,
message: mail
})
},
handleComment(comment) {
this.postComment({
id: this.item.id,
message: comment
})
},
changeTicketStatus(item) {
item.state = this.selected_state;
this.updateTicketPartial({
@ -112,13 +146,13 @@ export default {
state: this.selected_state,
})
},
//assignTicket(item) {
// item.assigned_to = this.selected_assignee;
// this.updateTicketPartial({
// id: item.id,
// assigned_to: this.selected_assignee
// })
//},
addCommentAndClear: async function () {
await this.postComment({
id: this.ticket.id,
message: this.newComment
})
this.newComment = "";
}
},
mounted() {
this.scheduleAfterInit(() => [Promise.all([this.loadEventItems(), this.loadBoxes()]).then(() => {

View file

@ -5,7 +5,7 @@
<template #body>
<EditItem
:item="editingItem"
badge="uid"
badge="id"
/>
</template>
<template #buttons>
@ -17,9 +17,9 @@
<div class="row" v-if="layout === 'table'">
<div class="col-xl-8 offset-xl-2">
<Table
:columns="['uid', 'description', 'box']"
:columns="['id', 'description', 'box']"
:items="getEventItems"
:keyName="'uid'"
:keyName="'id'"
@itemActivated="showItemDetail"
>
<template #actions="{ item }">
@ -43,9 +43,9 @@
</div>
<Cards
v-if="layout === 'cards'"
:columns="['uid', 'description', 'box']"
:columns="['id', 'description', 'box']"
:items="getEventItems"
:keyName="'uid'"
:keyName="'id'"
v-slot="{ item }"
@itemActivated="showItemDetail"
>
@ -55,7 +55,7 @@
/>
<div class="card-body">
<h6 class="card-title">{{ item.description }}</h6>
<h6 class="card-subtitle text-secondary">uid: {{ item.uid }} box: {{ item.box }}</h6>
<h6 class="card-subtitle text-secondary">id: {{ item.id }} box: {{ item.box }}</h6>
<div class="row mx-auto mt-2">
<div class="btn-group">
<button class="btn btn-outline-success"

View file

@ -1,5 +1,5 @@
<template>
<AsyncLoader :loaded="ticket.id">
<AsyncLoader :loaded="!!ticket.id">
<div class="container-fluid px-xl-5 mt-3">
<div class="row">
<div class="col-xl-8 offset-xl-2">
@ -7,7 +7,42 @@
<div class="card-header">
<h3>Ticket #{{ ticket.id }} - {{ ticket.name }}</h3>
</div>
<Timeline :timeline="ticket.timeline" @sendMail="handleMail" @addComment="handleComment"/>
<Timeline :timeline="ticket.timeline">
<template v-slot:timeline_action1>
<span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="comment"/>
</span>
<div class="new-comment card bg-dark">
<div>
<textarea placeholder="add comment..." v-model="newComment"
class="form-control">
</textarea>
<AsyncButton class="btn btn-primary float-right" :task="addCommentAndClear">
<font-awesome-icon icon="comment"/>
Save Comment
</AsyncButton>
</div>
</div>
</template>
<template v-slot:timeline_action2>
<span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="envelope"/>
</span>
<div class="new-mail card bg-dark">
<div class="card-header">
{{ newestMailSubject }}
</div>
<div>
<textarea placeholder="reply mail..." v-model="newMail" class="form-control">
</textarea>
<AsyncButton class="btn btn-primary float-right" :task="sendMailAndClear">
<font-awesome-icon icon="envelope"/>
Send Mail
</AsyncButton>
</div>
</div>
</template>
</Timeline>
<div class="card-footer d-flex justify-content-between">
<button class="btn btn-secondary mr-2" @click="$router.go(-1)">Back</button>
<!--button class="btn btn-danger" @click="deleteItem({type: 'tickets', id: ticket.id})">
@ -16,11 +51,14 @@
</button-->
<div class="btn-group">
<select class="form-control" v-model="selected_assignee">
<option v-for="user in users" :value="user.username">{{ user.username }}</option>
<option v-for="user in users" :value="user.username">{{
user.username
}}
</option>
</select>
<button class="form-control btn btn-success"
@click="assignTicket(ticket)"
:disabled="!selected_assignee || (selected_assignee == ticket.assigned_to)">
:disabled="!selected_assignee || (selected_assignee === ticket.assigned_to)">
Assign&nbsp;Ticket
</button>
</div>
@ -33,7 +71,7 @@
</select>
<button class="form-control btn btn-success"
@click="changeTicketStatus(ticket)"
:disabled="(selected_state == ticket.state)">
:disabled="(selected_state === ticket.state)">
Change&nbsp;Status
</button>
</div>
@ -58,6 +96,30 @@
</div>
</div>
</div>
<div class="col-lg-3 col-xl-2 d-lg-none d-xl-block"
v-if="ticket.related_items && ticket.related_items.length">
<div class="card bg-dark text-light mb-2" id="filters">
<div class="card-body">
<h5 class="card-title text-info">Related</h5>
<div class="card bg-dark" v-for="item in ticket.related_items" v-bind:key="item.id">
<AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`"
class="d-block card-img"
@click="openLightboxModalWith(item)"
/>
<div class="card-body">
<!--h6 class="card-title text-info"><span class="badge badge-primary">{{ item.relation_status }}</span></--h6-->
<h6 class="card-subtitle text-secondary">id: {{ item.id }} box: {{
item.box
}}</h6>
<router-link :to="{name: 'item', params: {id: item.id}}">
<h6 class="card-title">{{ item.description }}</h6>
</router-link>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AsyncLoader>
@ -68,15 +130,19 @@ import {mapActions, mapGetters, mapState} from 'vuex';
import Timeline from "@/components/Timeline.vue";
import ClipboardButton from "@/components/inputs/ClipboardButton.vue";
import AsyncLoader from "@/components/AsyncLoader.vue";
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AsyncButton from "@/components/inputs/AsyncButton.vue";
export default {
name: 'Ticket',
components: {AsyncLoader, ClipboardButton, Timeline},
components: {AsyncButton, AuthenticatedImage, AsyncLoader, ClipboardButton, Timeline},
data() {
return {
selected_state: null,
selected_assignee: null,
shipping_voucher_type: null,
newMail: "",
newComment: ""
}
},
computed: {
@ -90,46 +156,51 @@ export default {
shippingEmail() {
const domain = document.location.hostname;
return `ticket+${this.ticket.uuid}@${domain}`;
}
},
newestMailSubject() {
const mail = this.ticket.timeline ? this.ticket.timeline.filter(item => item.type === 'mail').pop() : null;
return mail ? mail.subject : "";
},
},
methods: {
...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']),
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']),
...mapActions(['claimShippingVoucher', 'fetchShippingVouchers']),
handleMail(mail) {
this.sendMail({
id: this.ticket.id,
message: mail
})
},
handleComment(comment) {
this.postComment({
id: this.ticket.id,
message: comment
})
},
changeTicketStatus(ticket) {
ticket.state = this.selected_state;
changeTicketStatus() {
this.ticket.state = this.selected_state;
this.updateTicketPartial({
id: ticket.id,
id: this.ticket.id,
state: this.selected_state,
})
},
assignTicket(ticket) {
ticket.assigned_to = this.selected_assignee;
assignTicket() {
this.ticket.assigned_to = this.selected_assignee;
this.updateTicketPartial({
id: ticket.id,
id: this.ticket.id,
assigned_to: this.selected_assignee
})
},
sendMailAndClear: async function () {
await this.sendMail({
id: this.ticket.id,
message: this.newMail,
})
this.newMail = "";
},
addCommentAndClear: async function () {
await this.postComment({
id: this.ticket.id,
message: this.newComment
})
this.newComment = "";
}
},
mounted() {
this.scheduleAfterInit(() => [Promise.all([this.fetchTicketStates(), this.loadTickets(), this.loadUsers(), this.fetchShippingVouchers()]).then(() => {
if (this.ticket.state == "pending_new") {
if (this.ticket.state === "pending_new") {
this.selected_state = "pending_open";
this.changeTicketStatus(this.ticket)
this.changeTicketStatus()
}
;
this.selected_state = this.ticket.state;
this.selected_assignee = this.ticket.assigned_to
})]);

View file

@ -12,11 +12,11 @@
>
<template v-slot:actions="{item}">
<div class="btn-group">
<a class="btn btn-primary" :href="'/'+ getEventSlug + '/ticket/' + item.id" title="view"
@click.prevent="gotoDetail(item)">
<router-link :to="{name: 'ticket', params: {id: item.id}}" class="btn btn-primary"
title="view">
<font-awesome-icon icon="eye"/>
View
</a>
</router-link>
</div>
</template>
</SlotTable>
@ -39,11 +39,11 @@
<td v-if="getEventSlug==='all'">{{ item.event }}</td>
<td>
<div class="btn-group">
<a class="btn btn-primary" :href="'/'+ getEventSlug + '/ticket/' + item.id" title="view"
@click.prevent="gotoDetail(item)">
<router-link :to="{name: 'ticket', params: {id: item.id}}" class="btn btn-primary"
title="view">
<font-awesome-icon icon="eye"/>
View
</a>
</router-link>
</div>
</td>
</tr>

View file

@ -88,11 +88,11 @@ export default {
if (!this.events[id].addresses.includes(a))
this.events[id].addresses.push(a)
this.new_address[id] = ""
this.updateEvent({id: this.events[id].eid, partial_event: {addresses: this.events[id].addresses}});
this.updateEvent({id: this.events[id].id, partial_event: {addresses: this.events[id].addresses}});
},
deleteAddress(id, a_id) {
this.events[id].addresses = this.events[id].addresses.filter((e, i) => i !== a_id);
this.updateEvent({id: this.events[id].eid, partial_event: {addresses: this.events[id].addresses}});
this.updateEvent({id: this.events[id].id, partial_event: {addresses: this.events[id].addresses}});
}
},
};