Compare commits

..

No commits in common. "594b8c4da36ec25a790ff41954c0951f890df55e" and "e005d6fb6ff91c7369baa2e1921b50e03ef747d7" have entirely different histories.

12 changed files with 185 additions and 441 deletions

View file

@ -22,7 +22,7 @@ class ContainerViewSet(viewsets.ModelViewSet):
@api_view(['GET']) @api_view(['GET'])
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
#@permission_required('view_item', raise_exception=True) @permission_required('view_item', raise_exception=True)
def search_items(request, event_slug, query): def search_items(request, event_slug, query):
try: try:
event = Event.objects.get(slug=event_slug) event = Event.objects.get(slug=event_slug)

View file

@ -1,10 +1,10 @@
<template> <template>
<div style="min-height: 100vh; display: flex; flex-direction: column;"> <div id="app">
<AddItemModal v-if="addItemModalOpen && isLoggedIn" @close="closeAddItemModal()" isModal="true"/> <AddItemModal v-if="addItemModalOpen && isLoggedIn" @close="closeAddItemModal()" isModal="true"/>
<AddTicketModal v-if="addTicketModalOpen && isLoggedIn" @close="closeAddTicketModal()" isModal="true"/> <AddTicketModal v-if="addTicketModalOpen && isLoggedIn" @close="closeAddTicketModal()" isModal="true"/>
<AddBoxModal v-if="showAddBoxModal && isLoggedIn" @close="closeAddBoxModal()" isModal="true"/> <AddBoxModal v-if="showAddBoxModal && isLoggedIn" @close="closeAddBoxModal()" isModal="true"/>
<Navbar v-if="isLoggedIn" @addItemClicked="openAddItemModal()" @addTicketClicked="openAddTicketModal()"/> <Navbar v-if="isLoggedIn" @addItemClicked="openAddItemModal()" @addTicketClicked="openAddTicketModal()"/>
<router-view style="flex: 1 1;"/> <router-view/>
</div> </div>
</template> </template>

View file

@ -1,133 +0,0 @@
<template>
<div class="async-wrapper" :class="{ 'loaded': loaded }">
<div class="deferred">
<slot></slot>
</div>
<div class="loader-wrapper">
<div class="loader-ellipsis">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'AsyncLoader',
props: {
loaded: {
type: Boolean,
default: false
}
}
};
</script>
<style>
.async-wrapper {
position: relative;
}
.async-wrapper > .deferred {
width: 100%;
height: 100%;
display: none;
}
.async-wrapper.loaded > .deferred {
display: block;
}
.async-wrapper > .loader-wrapper {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis {
color: #17a2b8;
}
.async-wrapper.loaded > .loader-wrapper > .loader-ellipsis {
display: none;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis,
.async-wrapper > .loader-wrapper > .loader-ellipsis div {
box-sizing: border-box;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis div {
position: absolute;
top: 33.33333px;
width: 13.33333px;
height: 13.33333px;
border-radius: 50%;
background: currentColor;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.async-wrapper > .loader-wrapper > .loader-ellipsis div:nth-child(1) {
left: 8px;
animation: loader-ellipsis1 0.6s infinite;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis div:nth-child(2) {
left: 8px;
animation: loader-ellipsis2 0.6s infinite;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis div:nth-child(3) {
left: 32px;
animation: loader-ellipsis2 0.6s infinite;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis div:nth-child(4) {
left: 56px;
animation: loader-ellipsis3 0.6s infinite;
}
@keyframes loader-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes loader-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes loader-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(24px, 0);
}
}
</style>

View file

@ -29,7 +29,16 @@
</router-link> </router-link>
</li> </li>
</ul> </ul>
<SearchBox v-if="hasPermissions" class="mt-1 my-lg-auto my-xl-auto w-100 d-inline mr-1"/> <form class="form-inline mt-1 my-lg-auto my-xl-auto w-100 d-inline mr-1" v-if="hasPermissions">
<input
class="form-control w-100"
type="search"
placeholder="Search"
aria-label="Search"
@input="searchEventItems($event.target.value)"
disabled
>
</form>
<div class="custom-control-inline mr-1" v-if="hasPermissions"> <div class="custom-control-inline mr-1" v-if="hasPermissions">
<div class="btn-group btn-group-toggle mr-1" v-if="isItemView()"> <div class="btn-group btn-group-toggle mr-1" v-if="isItemView()">
<button :class="['btn', 'btn-info', { active: layout === 'cards' }]" @click="setLayout('cards')"> <button :class="['btn', 'btn-info', { active: layout === 'cards' }]" @click="setLayout('cards')">
@ -94,13 +103,9 @@
<script> <script>
import {mapState, mapActions, mapMutations, mapGetters} from 'vuex'; import {mapState, mapActions, mapMutations, mapGetters} from 'vuex';
import SearchBox from "@/components/inputs/SearchBox.vue";
export default { export default {
name: 'Navbar', name: 'Navbar',
components: {
SearchBox
},
data: () => ({ data: () => ({
views: [ views: [
{'title': 'items', 'path': 'items'}, {'title': 'items', 'path': 'items'},
@ -117,7 +122,7 @@ export default {
...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions", "layout", "route"]), ...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions", "layout", "route"]),
}, },
methods: { methods: {
...mapActions(['changeEvent', 'changeView']), ...mapActions(['changeEvent', 'changeView', 'searchEventItems']),
...mapMutations(['logout']), ...mapMutations(['logout']),
navigateTo(link) { navigateTo(link) {
if (this.route.path !== link) if (this.route.path !== link)

View file

@ -40,10 +40,10 @@
<div class=""> <div class="">
<textarea placeholder="add comment..." v-model="newComment" class="form-control"> <textarea placeholder="add comment..." v-model="newComment" class="form-control">
</textarea> </textarea>
<AsyncButton class="btn btn-primary float-right" :task="addCommentAndClear"> <button class="btn btn-primary float-right" @click="addCommentAndClear">
<font-awesome-icon icon="comment"/> <font-awesome-icon icon="comment"/>
Save Comment Save Comment
</AsyncButton> </button>
</div> </div>
</div> </div>
</li> </li>
@ -58,10 +58,10 @@
<div> <div>
<textarea placeholder="reply mail..." v-model="newMail" class="form-control"> <textarea placeholder="reply mail..." v-model="newMail" class="form-control">
</textarea> </textarea>
<AsyncButton class="btn btn-primary float-right" :task="sendMailAndClear"> <button class="btn btn-primary float-right" @click="sendMailAndClear">
<font-awesome-icon icon="envelope"/> <font-awesome-icon icon="envelope"/>
Send Mail Send Mail
</AsyncButton> </button>
</div> </div>
</div> </div>
</li> </li>
@ -73,16 +73,15 @@
import TimelineMail from "@/components/TimelineMail.vue"; import TimelineMail from "@/components/TimelineMail.vue";
import TimelineComment from "@/components/TimelineComment.vue"; import TimelineComment from "@/components/TimelineComment.vue";
import TimelineStateChange from "@/components/TimelineStateChange.vue"; import TimelineStateChange from "@/components/TimelineStateChange.vue";
import {mapActions, 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 TimelineRelatedItem from "@/components/TimelineRelatedItem.vue";
import TimelineShippingVoucher from "@/components/TimelineShippingVoucher.vue"; import TimelineShippingVoucher from "@/components/TimelineShippingVoucher.vue";
import AsyncButton from "@/components/inputs/AsyncButton.vue";
export default { export default {
name: 'Timeline', name: 'Timeline',
components: { components: {
TimelineShippingVoucher, AsyncButton, TimelineShippingVoucher,
TimelineRelatedItem, TimelineAssignment, TimelineStateChange, TimelineComment, TimelineMail TimelineRelatedItem, TimelineAssignment, TimelineStateChange, TimelineComment, TimelineMail
}, },
props: { props: {
@ -104,15 +103,12 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['sendMail', 'postComment']), sendMailAndClear: function () {
sendMailAndClear: async function () { this.$emit('sendMail', this.newMail);
//this.$emit('sendMail', this.newMail);
await this.sendMail(this.newMail);
this.newMail = ""; this.newMail = "";
}, },
addCommentAndClear: async function () { addCommentAndClear: function () {
//this.$emit('addComment', this.newComment); this.$emit('addComment', this.newComment);
await this.postComment(this.newComment);
this.newComment = ""; this.newComment = "";
} }
} }

View file

@ -1,47 +0,0 @@
<template>
<button @click.stop="handleClick" :disabled="disabled">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"
:class="{'d-none': !disabled}"></span>
<span class="ml-2" :class="{'d-none': !disabled}">In Progress...</span>
<span :class="{'d-none': disabled}"><slot></slot></span>
</button>
</template>
<script>
export default {
name: 'AsyncButton',
data() {
return {
disabled: false,
};
},
props: {
task: {
type: Function,
required: true,
},
},
methods: {
async handleClick() {
console.log("AsyncButton.handleClick() called");
if (this.task && typeof this.task === 'function') {
this.disabled = true;
try {
await this.task();
} catch (e) {
console.error(e);
} finally {
this.disabled = false;
}
}
},
}
};
</script>
<style scoped>
.spinner-border {
vertical-align: -0.125em;
}
</style>

View file

@ -1,29 +0,0 @@
<template>
<form class="form-inline">
<input
class="form-control w-100"
type="search"
placeholder="Search"
aria-label="Search"
v-model="search_query"
@input="searchEventItems(search_query)"
>
</form>
</template>
<script>
import {mapActions} from "vuex";
export default {
name: 'SearchBox',
data() {
return {
search_query: ''
}
},
methods: {
...mapActions(['searchEventItems']),
}
};
</script>

View file

@ -46,7 +46,6 @@ const store = createStore({
states: 0, states: 0,
messageTemplates: 0, messageTemplates: 0,
shippingVouchers: 0, shippingVouchers: 0,
userNotificationChannels: 0,
}, },
persistent_loaded: false, persistent_loaded: false,
shared_loaded: false, shared_loaded: false,
@ -234,10 +233,6 @@ const store = createStore({
state.shippingVouchers = codes; state.shippingVouchers = codes;
state.fetchedData = {...state.fetchedData, shippingVouchers: Date.now()}; state.fetchedData = {...state.fetchedData, shippingVouchers: Date.now()};
}, },
setUserNotificationChannels(state, channels) {
state.userNotificationChannels = channels;
state.fetchedData = {...state.fetchedData, userNotificationChannels: Date.now()};
},
}, },
actions: { actions: {
async login({commit}, {username, password, remember}) { async login({commit}, {username, password, remember}) {
@ -515,10 +510,9 @@ const store = createStore({
}, },
async fetchUserNotificationChannels({commit, state}) { async fetchUserNotificationChannels({commit, state}) {
if (!state.user.token) return; if (!state.user.token) return;
if (state.fetchedData.userNotificationChannels > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/user_notification_channels/', state.user.token); const {data, success} = await http.get('/2/user_notification_channels/', state.user.token);
if (data && success) { if (data && success) {
commit('setUserNotificationChannels', data); state.userNotificationChannels = data;
} }
}, },
}, },

View file

@ -1,5 +1,4 @@
<template> <template>
<AsyncLoader :loaded="loadedItems.length > 0">
<div class="container-fluid px-xl-5 mt-3"> <div class="container-fluid px-xl-5 mt-3">
<Modal title="Edit Item" v-if="editingItem" @close="closeEditingModal()"> <Modal title="Edit Item" v-if="editingItem" @close="closeEditingModal()">
<template #body> <template #body>
@ -25,8 +24,7 @@
<template #actions="{ item }"> <template #actions="{ item }">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-success" <button class="btn btn-success"
@click.stop="confirm('return Item?') && markItemReturned(item)" @click.stop="confirm('return Item?') && markItemReturned(item)" title="returned">
title="returned">
<font-awesome-icon icon="check"/> <font-awesome-icon icon="check"/>
</button> </button>
<button class="btn btn-secondary" @click.stop="openEditingModalWith(item)" title="edit"> <button class="btn btn-secondary" @click.stop="openEditingModalWith(item)" title="edit">
@ -62,12 +60,10 @@
@click.stop="confirm('return Item?') && markItemReturned(item)" title="returned"> @click.stop="confirm('return Item?') && markItemReturned(item)" title="returned">
<font-awesome-icon icon="check"/> <font-awesome-icon icon="check"/>
</button> </button>
<button class="btn btn-outline-secondary" @click.stop="openEditingModalWith(item)" <button class="btn btn-outline-secondary" @click.stop="openEditingModalWith(item)" title="edit">
title="edit">
<font-awesome-icon icon="edit"/> <font-awesome-icon icon="edit"/>
</button> </button>
<button class="btn btn-outline-danger" <button class="btn btn-outline-danger" @click.stop="confirm('delete Item?') && deleteItem(item)"
@click.stop="confirm('delete Item?') && deleteItem(item)"
title="delete"> title="delete">
<font-awesome-icon icon="trash"/> <font-awesome-icon icon="trash"/>
</button> </button>
@ -76,7 +72,6 @@
</div> </div>
</Cards> </Cards>
</div> </div>
</AsyncLoader>
</template> </template>
<script> <script>
@ -87,7 +82,6 @@ import EditItem from '@/components/EditItem';
import {mapActions, mapGetters, mapState} from 'vuex'; import {mapActions, mapGetters, mapState} from 'vuex';
import Lightbox from '../components/Lightbox'; import Lightbox from '../components/Lightbox';
import AuthenticatedImage from "@/components/AuthenticatedImage.vue"; import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AsyncLoader from "@/components/AsyncLoader.vue";
export default { export default {
name: 'Items', name: 'Items',
@ -95,7 +89,7 @@ export default {
lightboxHash: null, lightboxHash: null,
editingItem: null, editingItem: null,
}), }),
components: {AsyncLoader, AuthenticatedImage, Lightbox, Table, Cards, Modal, EditItem}, components: {AuthenticatedImage, Lightbox, Table, Cards, Modal, EditItem},
computed: { computed: {
...mapState(['loadedItems']), ...mapState(['loadedItems']),
...mapGetters(['layout']), ...mapGetters(['layout']),

View file

@ -1,5 +1,4 @@
<template> <template>
<AsyncLoader :loaded="ticket.id">
<div class="container-fluid px-xl-5 mt-3"> <div class="container-fluid px-xl-5 mt-3">
<div class="row"> <div class="row">
<div class="col-xl-8 offset-xl-2"> <div class="col-xl-8 offset-xl-2">
@ -18,16 +17,13 @@
<select class="form-control" v-model="ticket.assigned_to"> <select class="form-control" v-model="ticket.assigned_to">
<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> </select>
<button class="form-control btn btn-success" @click="assignTicket(ticket)"> <button class="form-control btn btn-success" @click="assigTicket(ticket)">
Assign&nbsp;Ticket Assign&nbsp;Ticket
</button> </button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<select class="form-control" v-model="ticket.state"> <select class="form-control" v-model="ticket.state">
<option v-for="status in state_options" :value="status.value">{{ <option v-for="status in state_options" :value="status.value">{{ status.text }}</option>
status.text
}}
</option>
</select> </select>
<button class="form-control btn btn-success" @click="changeTicketStatus(ticket)"> <button class="form-control btn btn-success" @click="changeTicketStatus(ticket)">
Change&nbsp;Status Change&nbsp;Status
@ -56,18 +52,16 @@
</div> </div>
</div> </div>
</div> </div>
</AsyncLoader>
</template> </template>
<script> <script>
import {mapActions, mapGetters, mapState} from 'vuex'; import {mapActions, mapGetters, mapState} from 'vuex';
import Timeline from "@/components/Timeline.vue"; import Timeline from "@/components/Timeline.vue";
import ClipboardButton from "@/components/inputs/ClipboardButton.vue"; import ClipboardButton from "@/components/inputs/ClipboardButton.vue";
import AsyncLoader from "@/components/AsyncLoader.vue";
export default { export default {
name: 'Ticket', name: 'Ticket',
components: {AsyncLoader, ClipboardButton, Timeline}, components: {ClipboardButton, Timeline},
data() { data() {
return { return {
shipping_voucher_type: null shipping_voucher_type: null
@ -108,7 +102,7 @@ export default {
state: ticket.state state: ticket.state
}) })
}, },
assignTicket(ticket) { assigTicket(ticket) {
this.updateTicketPartial({ this.updateTicketPartial({
id: ticket.id, id: ticket.id,
assigned_to: ticket.assigned_to assigned_to: ticket.assigned_to

View file

@ -1,5 +1,4 @@
<template> <template>
<AsyncLoader :loaded="tickets.length > 0">
<div class="container-fluid px-xl-5 mt-3"> <div class="container-fluid px-xl-5 mt-3">
<div class="row"> <div class="row">
<div class="col-xl-8 offset-xl-2"> <div class="col-xl-8 offset-xl-2">
@ -47,7 +46,6 @@
</template> </template>
</CollapsableCards> </CollapsableCards>
</div> </div>
</AsyncLoader>
</template> </template>
<script> <script>
@ -58,11 +56,10 @@ import {mapActions, mapGetters, mapState} from 'vuex';
import Lightbox from '../components/Lightbox'; import Lightbox from '../components/Lightbox';
import SlotTable from "@/components/SlotTable.vue"; import SlotTable from "@/components/SlotTable.vue";
import CollapsableCards from "@/components/CollapsableCards.vue"; import CollapsableCards from "@/components/CollapsableCards.vue";
import AsyncLoader from "@/components/AsyncLoader.vue";
export default { export default {
name: 'Tickets', name: 'Tickets',
components: {AsyncLoader, Lightbox, SlotTable, Cards, Modal, EditItem, CollapsableCards}, components: {Lightbox, SlotTable, Cards, Modal, EditItem, CollapsableCards},
computed: { computed: {
...mapState(['tickets']), ...mapState(['tickets']),
...mapGetters(['stateInfo', 'getEventSlug', 'layout']), ...mapGetters(['stateInfo', 'getEventSlug', 'layout']),

View file

@ -1,36 +1,9 @@
<template> <template>
<div> <ul>
<Table :items="userNotificationChannels.map(channel => ({...channel, username: channel.user.username || {}}))" <li v-for="channel in userNotificationChannels" :key="channel.id">
:columns="['id', 'username', 'channel_type', 'channel_target', 'event_filter', /*'active', 'created'*/]"> {{ channel.id }} - {{ channel.channel_type }} - {{ channel.channel_target }} - {{ channel.event_filter }} - {{ channel.active }} - {{ channel.created }} - {{ channel.user }}
<template #actions="{ item }"> </li>
<div class="btn-group"> </ul>
<button class="btn btn-danger" @click.stop="">
<font-awesome-icon icon="trash"/>
delete
</button>
</div>
</template>
</Table>
<div class="card bg-dark">
<div class="card-body">
<div class="input-group">
<select class="form-control">
<option value="1">user</option>
<option value="2">admin</option>
</select>
<select class="form-control">
<option value="email">Email</option>
<option value="telegram">Telegram</option>
</select>
<input type="text" class="form-control" placeholder="channel_target">
<input type="text" class="form-control" value="*">
<div class="input-group-append">
<button class="btn btn-primary">Add</button>
</div>
</div>
</div>
</div>
</div>
</template> </template>
<script> <script>