add basic view for item history
This commit is contained in:
parent
a59423780e
commit
343040fa6d
14 changed files with 570 additions and 169 deletions
|
@ -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>
|
|
@ -52,7 +52,7 @@ export default {
|
|||
};
|
||||
},
|
||||
created() {
|
||||
const query = this.$router.currentRoute ? (this.$router.currentRoute.query ? this.$router.currentRoute.query.collapsed : null) : null;
|
||||
const query = this.route ? (this.route.query ? this.route.query.collapsed : null) : null;
|
||||
if (query !== null && query !== undefined) {
|
||||
this.collapsed = this.unpackInt(parseInt(query), this.sections.length);
|
||||
} else {
|
||||
|
@ -84,8 +84,8 @@ export default {
|
|||
const encoded = this.packInt(this.collapsed).toString()
|
||||
if (this.route.query.collapsed !== encoded)
|
||||
this.$router.push({
|
||||
...this.$router.currentRoute,
|
||||
query: {...this.$router.currentRoute.query, collapsed: encoded}
|
||||
...this.route,
|
||||
query: {...this.route.query, collapsed: encoded}
|
||||
});
|
||||
},
|
||||
deep: true,
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
field="description"
|
||||
:validation-fn="str => str && str.length > 0"
|
||||
/>
|
||||
<div class="form-group">
|
||||
<label for="box">box</label>
|
||||
<InputCombo
|
||||
label="box"
|
||||
:model="item"
|
||||
|
@ -20,6 +22,7 @@
|
|||
:options="boxes"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
85
web/src/components/TimelinePlacement.vue
Normal file
85
web/src/components/TimelinePlacement.vue
Normal 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>
|
|
@ -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"
|
||||
|
|
98
web/src/components/TimelineRelatedTicket.vue
Normal file
98
web/src/components/TimelineRelatedTicket.vue
Normal 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>
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<div class="form-group">
|
||||
<label :for="label">{{ label }}</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<button
|
||||
|
@ -34,7 +32,6 @@
|
|||
</div>
|
||||
<Addon type="Combo Box" :is-valid="isValid"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -27,7 +27,7 @@ const routes = [
|
|||
{requiresAuth: true, requiresPermission: 'view_item'}
|
||||
},
|
||||
{
|
||||
path: '/:event/item/:uid/', name: 'item', component: Items, meta:
|
||||
path: '/:event/item/:id/', name: 'item', component: Item, meta:
|
||||
{requiresAuth: true, requiresPermission: 'view_item'}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -145,17 +145,15 @@ const store = createStore({
|
|||
setItems(state, {slug, items}) {
|
||||
state.loadedItems[slug] = items;
|
||||
state.loadedItems = {...state.loadedItems};
|
||||
console.log(state.loadedItems)
|
||||
},
|
||||
replaceItems(state, items) {
|
||||
const groups = Object.groupBy(items, i => i.event ? i.event : 'none')
|
||||
for (const [key, value] of Object.entries(groups)) state.loadedItems[key] = value;
|
||||
state.loadedItems = {...state.loadedItems};
|
||||
console.log(state.loadedItems)
|
||||
},
|
||||
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) {
|
||||
|
@ -167,13 +165,11 @@ const store = createStore({
|
|||
setTickets(state, {slug, tickets}) {
|
||||
state.loadedTickets[slug] = tickets;
|
||||
state.loadedTickets = {...state.loadedTickets};
|
||||
console.log(state.loadedTickets)
|
||||
},
|
||||
replaceTickets(state, tickets) {
|
||||
const groups = Object.groupBy(tickets, t => t.event ? t.event : 'none')
|
||||
for (const [key, value] of Object.entries(groups)) state.loadedTickets[key] = value;
|
||||
state.loadedTickets = {...state.loadedTickets};
|
||||
console.log(state.loadedTickets)
|
||||
},
|
||||
updateTicket(state, updatedTicket) {
|
||||
const ticket = state.loadedTickets[updatedTicket.event ? updatedTicket.event : 'none'].filter(
|
||||
|
@ -336,14 +332,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}) {
|
||||
|
@ -405,16 +400,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) {
|
||||
|
|
168
web/src/views/Item.vue
Normal file
168
web/src/views/Item.vue
Normal file
|
@ -0,0 +1,168 @@
|
|||
<template>
|
||||
<AsyncLoader :loaded="!!item.id">
|
||||
<div class="container-fluid px-xl-5 mt-3">
|
||||
<div class="row">
|
||||
<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>
|
||||
<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>
|
||||
</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">
|
||||
<button class="btn btn-outline-success"
|
||||
@click.stop="confirm('return Item?') && markItemReturned(item)"
|
||||
title="returned">
|
||||
<font-awesome-icon icon="check"/>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" @click.stop="openEditingModalWith(item)"
|
||||
title="edit">
|
||||
<font-awesome-icon icon="edit"/>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger"
|
||||
@click.stop="confirm('delete Item?') && deleteItem(item)"
|
||||
title="delete">
|
||||
<font-awesome-icon icon="trash"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<InputCombo
|
||||
label="box"
|
||||
:model="item"
|
||||
nameKey="box"
|
||||
uniqueKey="cid"
|
||||
:options="boxes"
|
||||
/>
|
||||
<div class="btn-group">
|
||||
<select class="form-control" v-model="selected_state">
|
||||
<option v-for="status in state_options" :value="status.value">{{
|
||||
status.text
|
||||
}}
|
||||
</option>
|
||||
</select>
|
||||
<button class="form-control btn btn-success"
|
||||
@click="changeTicketStatus(item)"
|
||||
:disabled="(selected_state == item.state)">
|
||||
Change Status
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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 InputCombo from "@/components/inputs/InputCombo.vue";
|
||||
import InputString from "@/components/inputs/InputString.vue";
|
||||
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
|
||||
|
||||
export default {
|
||||
name: 'Item',
|
||||
components: {AuthenticatedImage, InputString, InputCombo, AsyncLoader, ClipboardButton, Timeline},
|
||||
data() {
|
||||
return {
|
||||
newComment: ""
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['state_options', 'users']),
|
||||
...mapGetters(['availableShippingVoucherTypes', 'getAllItems', 'route', 'getBoxes']),
|
||||
item() {
|
||||
const id = parseInt(this.route.params.id)
|
||||
const ret = this.getAllItems.find(item => item.id === id);
|
||||
return ret ? ret : {};
|
||||
},
|
||||
boxes() {
|
||||
console.log(this.getBoxes);
|
||||
return this.getBoxes.map(obj => ({cid: obj.cid, box: obj.name}));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['deleteItem', 'markItemReturned', 'updateTicketPartial', 'postComment']),
|
||||
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']),
|
||||
...mapActions(['claimShippingVoucher', 'fetchShippingVouchers', 'loadEventItems', 'loadBoxes']),
|
||||
changeTicketStatus(item) {
|
||||
item.state = this.selected_state;
|
||||
this.updateTicketPartial({
|
||||
id: item.id,
|
||||
state: this.selected_state,
|
||||
})
|
||||
},
|
||||
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(() => {
|
||||
this.selected_state = this.item.state;
|
||||
this.selected_assignee = this.item.assigned_to
|
||||
})]);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -5,7 +5,7 @@
|
|||
<template #body>
|
||||
<EditItem
|
||||
:item="editingItem"
|
||||
badge="uid"
|
||||
badge="id"
|
||||
/>
|
||||
</template>
|
||||
<template #buttons>
|
||||
|
@ -17,10 +17,10 @@
|
|||
<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'"
|
||||
@itemActivated="openLightboxModalWith($event)"
|
||||
:keyName="'id'"
|
||||
@itemActivated="showItemDetail"
|
||||
>
|
||||
<template #actions="{ item }">
|
||||
<div class="btn-group">
|
||||
|
@ -43,11 +43,11 @@
|
|||
</div>
|
||||
<Cards
|
||||
v-if="layout === 'cards'"
|
||||
:columns="['uid', 'description', 'box']"
|
||||
:columns="['id', 'description', 'box']"
|
||||
:items="getEventItems"
|
||||
:keyName="'uid'"
|
||||
:keyName="'id'"
|
||||
v-slot="{ item }"
|
||||
@itemActivated="openLightboxModalWith($event)"
|
||||
@itemActivated="showItemDetail"
|
||||
>
|
||||
<AuthenticatedImage v-if="item.file" cached
|
||||
:src="`/media/2/256/${item.file}/`"
|
||||
|
@ -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"
|
||||
|
@ -88,6 +88,7 @@ import {mapActions, mapGetters, mapState} from 'vuex';
|
|||
import Lightbox from '../components/Lightbox';
|
||||
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
|
||||
import AsyncLoader from "@/components/AsyncLoader.vue";
|
||||
import router from "@/router";
|
||||
|
||||
export default {
|
||||
name: 'Items',
|
||||
|
@ -102,6 +103,9 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem', 'scheduleAfterInit']),
|
||||
showItemDetail(item) {
|
||||
router.push({name: 'item', params: {id: item.id}});
|
||||
},
|
||||
openLightboxModalWith(item) {
|
||||
this.lightboxHash = item.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 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 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
|
||||
})]);
|
||||
|
|
|
@ -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>
|
||||
</Table>
|
||||
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue