add timeline nodes for linked items and shipping vouchers
This commit is contained in:
parent
a59509a432
commit
2f354130da
5 changed files with 373 additions and 5 deletions
|
@ -8,12 +8,19 @@
|
||||||
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'comment'">
|
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'comment'">
|
||||||
<font-awesome-icon icon="comment"/>
|
<font-awesome-icon icon="comment"/>
|
||||||
</span>
|
</span>
|
||||||
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'state'" :class="'bg-' + stateInfo(item.state).color">
|
<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"/>
|
<font-awesome-icon :icon="stateInfo(item.state).icon"/>
|
||||||
</span>
|
</span>
|
||||||
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'assignment'" :class="'bg-secondary'">
|
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'assignment'" :class="'bg-secondary'">
|
||||||
<font-awesome-icon icon="user"/>
|
<font-awesome-icon icon="user"/>
|
||||||
</span>
|
</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>
|
<span class="timeline-item-icon faded-icon" v-else>
|
||||||
<font-awesome-icon icon="pen"/>
|
<font-awesome-icon icon="pen"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -21,6 +28,8 @@
|
||||||
<TimelineComment v-else-if="item.type === 'comment'" :item="item"/>
|
<TimelineComment v-else-if="item.type === 'comment'" :item="item"/>
|
||||||
<TimelineStateChange v-else-if="item.type === 'state'" :item="item"/>
|
<TimelineStateChange v-else-if="item.type === 'state'" :item="item"/>
|
||||||
<TimelineAssignment v-else-if="item.type === 'assignment'" :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>
|
<p v-else>{{ item }}</p>
|
||||||
</li>
|
</li>
|
||||||
<li class="timeline-item">
|
<li class="timeline-item">
|
||||||
|
@ -66,10 +75,15 @@ import TimelineComment from "@/components/TimelineComment.vue";
|
||||||
import TimelineStateChange from "@/components/TimelineStateChange.vue";
|
import TimelineStateChange from "@/components/TimelineStateChange.vue";
|
||||||
import {mapGetters} from "vuex";
|
import {mapGetters} from "vuex";
|
||||||
import TimelineAssignment from "@/components/TimelineAssignment.vue";
|
import TimelineAssignment from "@/components/TimelineAssignment.vue";
|
||||||
|
import TimelineRelatedItem from "@/components/TimelineRelatedItem.vue";
|
||||||
|
import TimelineShippingVoucher from "@/components/TimelineShippingVoucher.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Timeline',
|
name: 'Timeline',
|
||||||
components: {TimelineAssignment, TimelineStateChange, TimelineComment, TimelineMail},
|
components: {
|
||||||
|
TimelineShippingVoucher,
|
||||||
|
TimelineRelatedItem, TimelineAssignment, TimelineStateChange, TimelineComment, TimelineMail
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
timeline: {
|
timeline: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
|
252
web/src/components/TimelineRelatedItem.vue
Normal file
252
web/src/components/TimelineRelatedItem.vue
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
<template>
|
||||||
|
<div class="timeline-item-wrapper">
|
||||||
|
<Lightbox v-if="lightboxHash" :hash="lightboxHash" @close="closeLightboxModal()"/>
|
||||||
|
<div class="timeline-item-description">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="card bg-dark">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" style="min-width: 4em;">
|
||||||
|
<AuthenticatedImage v-if="item.item.file" cached
|
||||||
|
:src="`/media/2/256/${item.item.file}/`"
|
||||||
|
class="d-block w-100 card-img-left"
|
||||||
|
@click="openLightboxModalWith(item.item)"
|
||||||
|
/>
|
||||||
|
</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-title">{{ item.item.description }}</h6>
|
||||||
|
<!--div class="row mx-auto mt-2">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-outline-success"
|
||||||
|
@click.stop="confirm('return Item?') && markItemReturned(item.item)"
|
||||||
|
title="returned">
|
||||||
|
<font-awesome-icon icon="check"/>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" @click.stop="openEditingModalWith(item.item)"
|
||||||
|
title="edit">
|
||||||
|
<font-awesome-icon icon="edit"/>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-danger"
|
||||||
|
@click.stop="confirm('delete Item?') && deleteItem(item.item)"
|
||||||
|
title="delete">
|
||||||
|
<font-awesome-icon icon="trash"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>{{ item }}</p-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--button class="show-replies">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-forward"
|
||||||
|
width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M15 11l4 4l-4 4m4 -4h-11a4 4 0 0 1 0 -8h1"/>
|
||||||
|
</svg>
|
||||||
|
Show 3 replies
|
||||||
|
<span class="avatar-list">
|
||||||
|
<i class="avatar | small">
|
||||||
|
<font-awesome-icon icon="user"/>
|
||||||
|
</i>
|
||||||
|
<i class="avatar | small">
|
||||||
|
<font-awesome-icon icon="user"/>
|
||||||
|
</i>
|
||||||
|
<i class="avatar | small">
|
||||||
|
<font-awesome-icon icon="user"/>
|
||||||
|
</i>
|
||||||
|
</span>
|
||||||
|
</button-->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
|
||||||
|
import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue";
|
||||||
|
import Lightbox from "@/components/Lightbox.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TimelineRelatedItem',
|
||||||
|
components: {Lightbox, AuthenticatedImage, AuthenticatedDataLink},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lightboxHash: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
'item': {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
'timestamp': function () {
|
||||||
|
return new Date(this.item.timestamp).toLocaleString();
|
||||||
|
},
|
||||||
|
'body': function () {
|
||||||
|
return this.item.body.replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br/>');
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openLightboxModalWith(attachment) {
|
||||||
|
this.lightboxHash = attachment.hash;
|
||||||
|
},
|
||||||
|
closeLightboxModal() { // Closes the editing modal and discards the edited copy of the item.
|
||||||
|
this.lightboxHash = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-img-left {
|
||||||
|
border-top-left-radius: calc(.25rem - 1px);
|
||||||
|
border-bottom-left-radius: calc(.25rem - 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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(--gray);
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 99em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--info);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.square {
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--gray);
|
||||||
|
background-color: var(--dark);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-replies {
|
||||||
|
color: var(--gray);
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: var(--info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 0 0 2px #fff;
|
||||||
|
background: var(--dark);
|
||||||
|
margin-right: -8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
92
web/src/components/TimelineShippingVoucher.vue
Normal file
92
web/src/components/TimelineShippingVoucher.vue
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<template>
|
||||||
|
<div class="timeline-item-description">
|
||||||
|
<i class="avatar | small">
|
||||||
|
<font-awesome-icon icon="user"/>
|
||||||
|
</i>
|
||||||
|
<span><a href="#">$USER</a> has claimed shipping voucher
|
||||||
|
<ClipboardButton class="btn btn-primary badge badge-pill" title="Copy shipping voucher to clipboard"
|
||||||
|
:payload="item.voucher">{{ item.voucher }}
|
||||||
|
<font-awesome-icon icon="clipboard"/>
|
||||||
|
</ClipboardButton> of type <span class="badge badge-pill badge-secondary">{{ item.voucher_type }}</span> for this ticket at <time
|
||||||
|
:datetime="timestamp">{{ timestamp }}</time>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {mapState} from "vuex";
|
||||||
|
import ClipboardButton from "@/components/inputs/ClipboardButton.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TimelineShippingVoucher',
|
||||||
|
components: {ClipboardButton},
|
||||||
|
props: {
|
||||||
|
'item': {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['state_options']),
|
||||||
|
'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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
|
@ -25,7 +25,15 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['state_options']),
|
...mapState(['state_options']),
|
||||||
lookupState: function () {
|
lookupState: function () {
|
||||||
|
try {
|
||||||
|
if (this.item.state)
|
||||||
return this.state_options.find(state => state.value === this.item.state);
|
return this.state_options.find(state => state.value === this.item.state);
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: 'Unknown',
|
||||||
|
value: 'unknown'
|
||||||
|
};
|
||||||
},
|
},
|
||||||
colorLookup: function () {
|
colorLookup: function () {
|
||||||
if (this.item.state.startsWith('closed_')) {
|
if (this.item.state.startsWith('closed_')) {
|
||||||
|
|
|
@ -39,13 +39,15 @@ import {
|
||||||
faClipboard,
|
faClipboard,
|
||||||
faTasks,
|
faTasks,
|
||||||
faAngleRight,
|
faAngleRight,
|
||||||
faAngleDown
|
faAngleDown,
|
||||||
|
faTruck,
|
||||||
|
faObjectGroup
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome';
|
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome';
|
||||||
|
|
||||||
library.add(faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList,
|
library.add(faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList,
|
||||||
faWindowClose, faCamera, faStop, faPen, faCheck, faTimes, faSave, faEye, faComment, faUser, faComments, faEnvelope,
|
faWindowClose, faCamera, faStop, faPen, faCheck, faTimes, faSave, faEye, faComment, faUser, faComments, faEnvelope,
|
||||||
faArchive, faMinus, faExclamation, faHourglass, faClipboard, faTasks, faAngleDown, faAngleRight);
|
faArchive, faMinus, faExclamation, faHourglass, faClipboard, faTasks, faAngleDown, faAngleRight, faTruck, faObjectGroup);
|
||||||
|
|
||||||
|
|
||||||
const app = createApp(App).use(store).use(router);
|
const app = createApp(App).use(store).use(router);
|
||||||
|
|
Loading…
Reference in a new issue