add frontend to manage shipping vouchers
This commit is contained in:
parent
f11758607e
commit
d8be7f09e4
5 changed files with 185 additions and 8 deletions
|
@ -15,6 +15,7 @@ import Empty from "@/views/Empty.vue";
|
||||||
import Events from "@/views/admin/Events.vue";
|
import Events from "@/views/admin/Events.vue";
|
||||||
import AccessControl from "@/views/admin/AccessControl.vue";
|
import AccessControl from "@/views/admin/AccessControl.vue";
|
||||||
import {default as BoxesAdmin} from "@/views/admin/Boxes.vue"
|
import {default as BoxesAdmin} from "@/views/admin/Boxes.vue"
|
||||||
|
import Shipping from "@/views/admin/Shipping.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{path: '/', redirect: '/37C3/items', meta: {requiresAuth: false}},
|
{path: '/', redirect: '/37C3/items', meta: {requiresAuth: false}},
|
||||||
|
@ -69,6 +70,10 @@ const routes = [
|
||||||
path: 'boxes/', name: 'admin_boxes', component: BoxesAdmin, meta:
|
path: 'boxes/', name: 'admin_boxes', component: BoxesAdmin, meta:
|
||||||
{requiresAuth: true, requiresPermission: 'delete_event'}
|
{requiresAuth: true, requiresPermission: 'delete_event'}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'shipping/', name: 'shipping', component: Shipping, meta:
|
||||||
|
{requiresAuth: true, requiresPermission: 'delete_event'}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{path: '/user', name: 'user', component: Empty, meta: {requiresAuth: true}},
|
{path: '/user', name: 'user', component: Empty, meta: {requiresAuth: true}},
|
||||||
|
|
|
@ -6,7 +6,6 @@ import * as utf8 from 'utf8';
|
||||||
import {ticketStateColorLookup, ticketStateIconLookup, http} from "@/utils";
|
import {ticketStateColorLookup, ticketStateIconLookup, http} from "@/utils";
|
||||||
import sharedStatePlugin from "@/shared-state-plugin";
|
import sharedStatePlugin from "@/shared-state-plugin";
|
||||||
import persistentStatePlugin from "@/persistent-state-plugin";
|
import persistentStatePlugin from "@/persistent-state-plugin";
|
||||||
import {triggerRef} from "vue";
|
|
||||||
|
|
||||||
const store = createStore({
|
const store = createStore({
|
||||||
state: {
|
state: {
|
||||||
|
@ -20,6 +19,8 @@ const store = createStore({
|
||||||
users: [],
|
users: [],
|
||||||
groups: [],
|
groups: [],
|
||||||
state_options: [],
|
state_options: [],
|
||||||
|
shippingVouchers: [],
|
||||||
|
|
||||||
lastEvent: '37C3',
|
lastEvent: '37C3',
|
||||||
lastUsed: {},
|
lastUsed: {},
|
||||||
remember: false,
|
remember: false,
|
||||||
|
@ -40,12 +41,22 @@ const store = createStore({
|
||||||
users: 0,
|
users: 0,
|
||||||
groups: 0,
|
groups: 0,
|
||||||
states: 0,
|
states: 0,
|
||||||
|
shippingVouchers: 0,
|
||||||
},
|
},
|
||||||
persistent_loaded: false,
|
persistent_loaded: false,
|
||||||
shared_loaded: false,
|
shared_loaded: false,
|
||||||
afterInitHandlers: [],
|
afterInitHandlers: [],
|
||||||
|
|
||||||
showAddBoxModal: false,
|
showAddBoxModal: false,
|
||||||
|
|
||||||
|
shippingVoucherTypes: {
|
||||||
|
'2kg-de': '2kg Paket (DE)',
|
||||||
|
'5kg-de': '5kg Paket (DE)',
|
||||||
|
'10kg-de': '10kg Paket (DE)',
|
||||||
|
'2kg-eu': '2kg Paket (EU)',
|
||||||
|
'5kg-eu': '5kg Paket (EU)',
|
||||||
|
'10kg-eu': '10kg Paket (EU)',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
route: state => router.currentRoute.value,
|
route: state => router.currentRoute.value,
|
||||||
|
@ -75,6 +86,12 @@ const store = createStore({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
availableShippingVoucherTypes: state => {
|
||||||
|
return Object.keys(state.shippingVoucherTypes).map(key => {
|
||||||
|
var count = state.shippingVouchers.filter(voucher => voucher.type === key && voucher.issue_thread === null).length;
|
||||||
|
return {id: key, count: count, name: state.shippingVoucherTypes[key]};
|
||||||
|
});
|
||||||
|
},
|
||||||
layout: (state, getters) => {
|
layout: (state, getters) => {
|
||||||
if (router.currentRoute.value.query.layout)
|
if (router.currentRoute.value.query.layout)
|
||||||
return router.currentRoute.value.query.layout;
|
return router.currentRoute.value.query.layout;
|
||||||
|
@ -139,7 +156,6 @@ const store = createStore({
|
||||||
updateTicket(state, updatedTicket) {
|
updateTicket(state, updatedTicket) {
|
||||||
const ticket = state.tickets.filter(({id}) => id === updatedTicket.id)[0];
|
const ticket = state.tickets.filter(({id}) => id === updatedTicket.id)[0];
|
||||||
Object.assign(ticket, updatedTicket);
|
Object.assign(ticket, updatedTicket);
|
||||||
//triggerRef(state.tickets);
|
|
||||||
state.tickets = [...state.tickets];
|
state.tickets = [...state.tickets];
|
||||||
},
|
},
|
||||||
replaceUsers(state, users) {
|
replaceUsers(state, users) {
|
||||||
|
@ -198,6 +214,10 @@ const store = createStore({
|
||||||
setThumbnail(state, {url, data}) {
|
setThumbnail(state, {url, data}) {
|
||||||
state.thumbnailCache[url] = data;
|
state.thumbnailCache[url] = data;
|
||||||
},
|
},
|
||||||
|
setShippingVouchers(state, codes) {
|
||||||
|
state.shippingVouchers = codes;
|
||||||
|
state.fetchedData = {...state.fetchedData, shippingVouchers: Date.now()};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async login({commit}, {username, password, remember}) {
|
async login({commit}, {username, password, remember}) {
|
||||||
|
@ -412,6 +432,33 @@ const store = createStore({
|
||||||
async updateTicketPartial({commit, state}, {id, ...ticket}) {
|
async updateTicketPartial({commit, state}, {id, ...ticket}) {
|
||||||
const {data, success} = await http.patch(`/2/tickets/${id}/`, ticket, state.user.token);
|
const {data, success} = await http.patch(`/2/tickets/${id}/`, ticket, state.user.token);
|
||||||
commit('updateTicket', data);
|
commit('updateTicket', data);
|
||||||
|
},
|
||||||
|
async fetchShippingVouchers({commit, state}) {
|
||||||
|
if (!state.user.token) return;
|
||||||
|
if (state.fetchedData.shippingVouchers > Date.now() - 1000 * 60 * 60 * 24) return;
|
||||||
|
const {data, success} = await http.get('/2/shipping_vouchers/', state.user.token);
|
||||||
|
if (data && success) {
|
||||||
|
commit('setShippingVouchers', data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async createShippingVoucher({dispatch, state}, code) {
|
||||||
|
const {data, success} = await http.post('/2/shipping_vouchers/', code, state.user.token);
|
||||||
|
if (data && success) {
|
||||||
|
state.fetchedData.shippingVouchers = 0;
|
||||||
|
dispatch('fetchShippingVouchers');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async claimShippingVoucher({dispatch, state}, {ticket, shipping_voucher_type}) {
|
||||||
|
const id = state.shippingVouchers.filter(voucher => voucher.type === shipping_voucher_type && voucher.issue_thread === null)[0].id;
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
success
|
||||||
|
} = await http.patch(`/2/shipping_vouchers/${id}/`, {issue_thread: ticket}, state.user.token);
|
||||||
|
if (data && success) {
|
||||||
|
state.fetchedData.shippingVouchers = 0;
|
||||||
|
state.fetchedData.tickets = 0;
|
||||||
|
await Promise.all([dispatch('loadTickets'), dispatch('fetchShippingVouchers')]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -427,7 +474,7 @@ const store = createStore({
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
sharedStatePlugin({
|
sharedStatePlugin({
|
||||||
debug: true,
|
debug: false,
|
||||||
isLoadedKey: "shared_loaded",
|
isLoadedKey: "shared_loaded",
|
||||||
clearingMutation: "logout",
|
clearingMutation: "logout",
|
||||||
afterInit: "afterSharedInit",
|
afterInit: "afterSharedInit",
|
||||||
|
@ -440,6 +487,7 @@ const store = createStore({
|
||||||
"groups",
|
"groups",
|
||||||
"loadedBoxes",
|
"loadedBoxes",
|
||||||
"loadedItems",
|
"loadedItems",
|
||||||
|
"shippingVouchers",
|
||||||
],
|
],
|
||||||
watch: [
|
watch: [
|
||||||
"test",
|
"test",
|
||||||
|
@ -450,6 +498,7 @@ const store = createStore({
|
||||||
"groups",
|
"groups",
|
||||||
"loadedBoxes",
|
"loadedBoxes",
|
||||||
"loadedItems",
|
"loadedItems",
|
||||||
|
"shippingVouchers",
|
||||||
],
|
],
|
||||||
mutations: [
|
mutations: [
|
||||||
//"replaceTickets",
|
//"replaceTickets",
|
||||||
|
|
|
@ -13,10 +13,6 @@
|
||||||
<font-awesome-icon icon="trash"/>
|
<font-awesome-icon icon="trash"/>
|
||||||
Delete
|
Delete
|
||||||
</button-->
|
</button-->
|
||||||
<ClipboardButton :payload="shippingEmail" class="btn btn-primary">
|
|
||||||
<font-awesome-icon icon="clipboard"/>
|
|
||||||
Copy DHL contact to clipboard
|
|
||||||
</ClipboardButton>
|
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<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>
|
||||||
|
@ -34,6 +30,24 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-footer d-flex justify-content-between">
|
||||||
|
<ClipboardButton :payload="shippingEmail" class="btn btn-primary">
|
||||||
|
<font-awesome-icon icon="clipboard"/>
|
||||||
|
Copy DHL contact to clipboard
|
||||||
|
</ClipboardButton>
|
||||||
|
<div class="btn-group">
|
||||||
|
<select class="form-control" v-model="shipping_voucher_type">
|
||||||
|
<option v-for="type in availableShippingVoucherTypes.filter(t=>t.count>0)"
|
||||||
|
:value="type.id">{{ type.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button class="form-control btn btn-success"
|
||||||
|
@click="claimShippingVoucher({ticket: ticket.id, shipping_voucher_type}).then(()=>shipping_voucher_type=null)"
|
||||||
|
:disabled="!shipping_voucher_type">
|
||||||
|
Claim Shipping Voucher
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,15 +55,21 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapActions, 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";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Ticket',
|
name: 'Ticket',
|
||||||
components: {ClipboardButton, Timeline},
|
components: {ClipboardButton, Timeline},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
shipping_voucher_type: null
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['tickets', 'state_options', 'users']),
|
...mapState(['tickets', 'state_options', 'users']),
|
||||||
|
...mapGetters(['availableShippingVoucherTypes']),
|
||||||
ticket() {
|
ticket() {
|
||||||
const id = parseInt(this.$route.params.id)
|
const id = parseInt(this.$route.params.id)
|
||||||
const ret = this.tickets.find(ticket => ticket.id === id);
|
const ret = this.tickets.find(ticket => ticket.id === id);
|
||||||
|
@ -63,6 +83,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']),
|
...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']),
|
||||||
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']),
|
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']),
|
||||||
|
...mapActions(['claimShippingVoucher']),
|
||||||
handleMail(mail) {
|
handleMail(mail) {
|
||||||
this.sendMail({
|
this.sendMail({
|
||||||
id: this.ticket.id,
|
id: this.ticket.id,
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<router-link class="nav-link" :to="{name: 'events'}" active-class="active">Events</router-link>
|
<router-link class="nav-link" :to="{name: 'events'}" active-class="active">Events</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<router-link class="nav-link" :to="{name: 'shipping'}" active-class="active">Shipping</router-link>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<router-link class="nav-link" :to="{name: 'admin_boxes'}" active-class="active">Boxes</router-link>
|
<router-link class="nav-link" :to="{name: 'admin_boxes'}" active-class="active">Boxes</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
|
99
web/src/views/admin/Shipping.vue
Normal file
99
web/src/views/admin/Shipping.vue
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3>Shipping Vouchers</h3>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h5>Shipping Voucher Types</h5>
|
||||||
|
<span v-for="(type, key) in availableShippingVoucherTypes" :key="key" class="mr-2">
|
||||||
|
<span v-if="type.count > 2" class="badge badge-success">{{ type.name }} - {{ type.count }}</span>
|
||||||
|
<span v-else-if="type.count > 0" class="badge badge-warning" v-if="type.count > 0">
|
||||||
|
{{ type.name }} - {{ type.count }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="badge badge-danger">{{ type.name }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h5>Available Shipping Vouchers</h5>
|
||||||
|
<ul>
|
||||||
|
<li v-for="voucher in shippingVouchers" :key="voucher.voucher">
|
||||||
|
<span v-if="voucher.issue_thread == null">{{ voucher.type }} - {{ voucher.voucher }}</span>
|
||||||
|
<span v-else><s style="color:var(--danger)">{{ voucher.type }} - {{ voucher.voucher }}</s> used in
|
||||||
|
<router-link :to="'/'+ getEventSlug + '/ticket/' + voucher.issue_thread">#{{
|
||||||
|
voucher.issue_thread
|
||||||
|
}}</router-link></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<textarea class="form-control mb-3" rows="5" placeholder="Shipping Voucher List" v-model="bulk_vouchers"
|
||||||
|
v-if="bulk"></textarea>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" placeholder="Shipping Voucher" v-model="voucher" v-if="!bulk">
|
||||||
|
<select class="form-control" v-model="type">
|
||||||
|
<option v-for="it in Object.keys(shippingVoucherTypes)" :value="it">{{
|
||||||
|
shippingVoucherTypes[it]
|
||||||
|
}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<div class="input-group-text">
|
||||||
|
<input type="checkbox" v-model="bulk" class="mr-2" id="bulk" style="margin: 0;">
|
||||||
|
<label for="bulk" style="margin: 0;">Bulk</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary form-control" @click="createSingleOrBulkShippingVoucher">
|
||||||
|
<font-awesome-icon icon="plus"/>
|
||||||
|
{{ (bulk ? "Add Shipping Vouchers" : "Add Shipping Voucher") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapActions, mapGetters, mapState} from 'vuex';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Shipping',
|
||||||
|
components: {Table},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
voucher: '',
|
||||||
|
bulk_vouchers: '',
|
||||||
|
type: '2kg-eu',
|
||||||
|
bulk: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['shippingVouchers', 'shippingVoucherTypes']),
|
||||||
|
...mapGetters(['getEventSlug', 'availableShippingVoucherTypes']),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['fetchShippingVouchers', 'createShippingVoucher']),
|
||||||
|
createSingleOrBulkShippingVoucher() {
|
||||||
|
if (this.bulk) {
|
||||||
|
const list = this.bulk_vouchers.split('\n');
|
||||||
|
if (confirm('Are you sure you want to add ' + list.length + ' shipping vouchers as ' + this.type + '?')) {
|
||||||
|
const jobs = list.map(voucher => {
|
||||||
|
return this.createShippingVoucher({voucher: voucher.trim(), type: this.type});
|
||||||
|
});
|
||||||
|
Promise.all(jobs).then(() => {
|
||||||
|
this.bulk_vouchers = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.createShippingVoucher({voucher: this.voucher, type: this.type}).then(() => {
|
||||||
|
this.voucher = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchShippingVouchers();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
Loading…
Reference in a new issue