add alternative layout for /tickets page

This commit is contained in:
j3d1 2024-01-17 20:08:28 +01:00
parent a3f6a96f95
commit b28bd7b23b
7 changed files with 192 additions and 16 deletions

View file

@ -0,0 +1,105 @@
<template>
<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-body">
<h5 class="card-title text-info">Sort & Filter</h5>
<div class="form-group" v-for="(column, index) in columns" :key="index">
<label>{{ column }}</label>
<div class="input-group">
<div class="input-group-prepend">
<button
:class="[ 'btn', column === sortBy ? 'btn-outline-info' : 'btn-outline-secondary' ]"
type="button"
@click="toggleSort(column)">
<font-awesome-icon :icon="getSortIcon(column)"/>
</button>
</div>
<input
type="text"
class="form-control"
placeholder="filter"
:value="filters[column]"
@input="changeFilter(column, $event.target.value)"
>
</div>
</div>
</div>
</div-->
</div>
<div class="col-lg-9 col-xl-8">
<div class="w-100"
v-for="(group, index) in grouped_items"
:key="index">
<div v-if="group.length>0" class="card card-list-item bg-dark w-100 mb-3">
<div class="card-header" :class="'bg-'+sections[index].color" @click="toggle(index)">
<font-awesome-icon icon="angle-right" style="width: 1em;" v-if="collapsed[index]"/>
<font-awesome-icon icon="angle-down" style="width: 1em;" v-else/>
<slot name="section_header" :index="index" :section="sections[index]" :count="group.length"/>
</div>
<table class="card-body collapse table table-striped table-dark" :class="{show: !collapsed[index]}">
<slot class="row" v-for="item in group" name="section_body" :item="item"/>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
import router from '../router';
export default {
name: 'CollapsableCards',
props: {
columns: {
type: Array,
required: true,
},
items: {
type: Array,
required: true,
},
sections: {
type: Array,
required: true,
},
keyName: {
type: String,
required: true,
},
},
data() {
return {
collapsed: [],
symbols: ['▲', '▼', '▶', '◀'],
};
},
created() {
this.collapsed = this.sections.map(() => true);
/*this.columns.map(e => ({
k: e,
v: this.$store.getters.getFilters[e]
})).filter(e => e.v).forEach(e => this.setFilter(e.k, e.v));*/
},
computed: {
grouped_items() {
return this.sections.map(section => this.items.filter(item => item[this.keyName] === section.slug));
}
},
methods: {
toggle(index) {
this.collapsed[index] = !this.collapsed[index];
this.$forceUpdate();
},
/*changeFilter(col, val) {
this.setFilter(col, val);
let newquery = Object.entries({
...this.$store.getters.getFilters,
[col]: val
}).reduce((a, [k, v]) => (v ? {...a, [k]: v} : a), {});
router.push({query: newquery});
},*/
},
};
</script>

View file

@ -48,6 +48,14 @@
<font-awesome-icon icon="list"/> <font-awesome-icon icon="list"/>
</button> </button>
</div> </div>
<div class="btn-group btn-group-toggle mr-1" v-if="isTicketView()">
<button :class="['btn', 'btn-info', { active: layout === 'tasks' }]" @click="setLayout('tasks')">
<font-awesome-icon icon="tasks"/>
</button>
<button :class="['btn', 'btn-info', { active: layout === 'table' }]" @click="setLayout('table')">
<font-awesome-icon icon="list"/>
</button>
</div>
<button type="button" class="btn text-nowrap btn-success mr-1" @click="$emit('addItemClicked')" <button type="button" class="btn text-nowrap btn-success mr-1" @click="$emit('addItemClicked')"
v-if="isItemView()"> v-if="isItemView()">
<font-awesome-icon icon="plus"/> <font-awesome-icon icon="plus"/>
@ -108,13 +116,13 @@ export default {
{'title': 'howto engel', 'path': '/howto/'}, {'title': 'howto engel', 'path': '/howto/'},
] ]
}), }),
emits: ['addItemClicked', 'addTicketClicked'],
computed: { computed: {
...mapState(['events', 'layout']), ...mapState(['events']),
...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions"]), ...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions", "layout"]),
}, },
methods: { methods: {
...mapActions(['changeEvent', 'changeView', 'searchEventItems']), ...mapActions(['changeEvent', 'changeView', 'searchEventItems']),
...mapMutations(['setLayout', 'logout']),
navigateTo(link) { navigateTo(link) {
if (this.$router.currentRoute.path !== link) if (this.$router.currentRoute.path !== link)
this.$router.push(link); this.$router.push(link);
@ -124,7 +132,12 @@ export default {
}, },
isTicketView() { isTicketView() {
return this.getActiveView === 'tickets' || this.getActiveView === 'ticket'; return this.getActiveView === 'tickets' || this.getActiveView === 'ticket';
} },
setLayout(layout) {
if (this.$router.currentRoute.query.layout === layout)
return;
this.$router.push({...this.$router.currentRoute, query: {...this.$router.currentRoute.query, layout}});
},
} }
}; };
</script> </script>

View file

@ -36,7 +36,13 @@ import {
faUser, faUser,
faComments, faComments,
faArchive, faArchive,
faMinus, faHourglass, faExclamation, faClipboard, faMinus,
faHourglass,
faExclamation,
faClipboard,
faTasks,
faAngleRight,
faAngleDown
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome';
@ -44,7 +50,7 @@ import vueDebounce from 'vue-debounce';
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); faArchive, faMinus, faExclamation, faHourglass, faClipboard, faTasks, faAngleDown, faAngleRight);
Vue.component('font-awesome-icon', FontAwesomeIcon); Vue.component('font-awesome-icon', FontAwesomeIcon);
sync(store, router); sync(store, router);

View file

@ -100,11 +100,14 @@ router.beforeEach((to, from, next) => {
} }
}); });
router.afterEach((to/*, from*/) => { router.afterEach((to, from) => {
if (to.params.event) { if (to.params.event) {
//console.log('update last event', to.params.event); //console.log('update last event', to.params.event);
store.commit('updateLastEvent', to.params.event); store.commit('updateLastEvent', to.params.event);
} }
if (to.query.layout !== from.query.layout) {
store.commit('triggerLayoutChange', to.query.layout);
}
}); });
export default router; export default router;

View file

@ -61,7 +61,6 @@ const store = new Vuex.Store({
state: { state: {
keyIncrement: 0, keyIncrement: 0,
events: [], events: [],
layout: 'cards',
loadedItems: [], loadedItems: [],
itemCache: {}, itemCache: {},
loadedBoxes: [], loadedBoxes: [],
@ -80,6 +79,7 @@ const store = new Vuex.Store({
token_expiry: null, token_expiry: null,
local_loaded: false, local_loaded: false,
showAddBoxModal: false, showAddBoxModal: false,
toggle: false,
}, },
getters: { getters: {
getEventSlug: state => state.route && state.route.params.event ? state.route.params.event : state.lastEvent, getEventSlug: state => state.route && state.route.params.event ? state.route.params.event : state.lastEvent,
@ -106,6 +106,15 @@ const store = new Vuex.Store({
} }
} }
}, },
layout: (state, getters) => {
state.toggle = !state.toggle;
if (router.currentRoute.query.layout)
return router.currentRoute.query.layout;
if (getters.getActiveView === 'items')
return 'cards';
if (getters.getActiveView === 'tickets')
return 'tasks';
},
isLoggedIn(state) { isLoggedIn(state) {
if (!state.local_loaded) { if (!state.local_loaded) {
state.remember = localStorage.getItem('remember') === 'true'; state.remember = localStorage.getItem('remember') === 'true';
@ -145,9 +154,6 @@ const store = new Vuex.Store({
setItemCache(state, {slug, items}) { setItemCache(state, {slug, items}) {
state.itemCache[slug] = items; state.itemCache[slug] = items;
}, },
setLayout(state, layout) {
state.layout = layout;
},
replaceBoxes(state, loadedBoxes) { replaceBoxes(state, loadedBoxes) {
state.loadedBoxes = loadedBoxes; state.loadedBoxes = loadedBoxes;
}, },
@ -223,6 +229,9 @@ const store = new Vuex.Store({
if (router.currentRoute.name !== 'login') if (router.currentRoute.name !== 'login')
router.push('/login'); router.push('/login');
}, },
triggerLayoutChange(state) {
state.toggle = !state.toggle;
}
}, },
actions: { actions: {
async login({commit, dispatch, state}, {username, password, remember}) { async login({commit, dispatch, state}, {username, password, remember}) {

View file

@ -77,7 +77,7 @@ import Table from '@/components/Table';
import Cards from '@/components/Cards'; import Cards from '@/components/Cards';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import EditItem from '@/components/EditItem'; import EditItem from '@/components/EditItem';
import {mapActions, 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";
@ -88,7 +88,10 @@ export default {
editingItem: null, editingItem: null,
}), }),
components: {AuthenticatedImage, Lightbox, Table, Cards, Modal, EditItem}, components: {AuthenticatedImage, Lightbox, Table, Cards, Modal, EditItem},
computed: mapState(['loadedItems', 'layout']), computed: {
...mapState(['loadedItems']),
...mapGetters(['layout']),
},
methods: { methods: {
...mapActions(['deleteItem', 'markItemReturned']), ...mapActions(['deleteItem', 'markItemReturned']),
openLightboxModalWith(item) { openLightboxModalWith(item) {

View file

@ -6,6 +6,7 @@
:columns="['id', 'name', 'state', 'last_activity', 'assigned_to']" :columns="['id', 'name', 'state', 'last_activity', 'assigned_to']"
:items="tickets" :items="tickets"
:keyName="'id'" :keyName="'id'"
v-if="layout === 'table'"
> >
<template #actions="{ item }"> <template #actions="{ item }">
<div class="btn-group"> <div class="btn-group">
@ -19,28 +20,64 @@
</Table> </Table>
</div> </div>
</div> </div>
<CollapsableCards v-if="layout === 'tasks'" :items="tickets"
:columns="['id', 'name', 'last_activity', 'assigned_to']"
:keyName="'state'" :sections="['pending_new', 'pending_open','pending_shipping',
'pending_physical_confirmation','pending_return'].map(stateInfo)">
<template #section_header="{index, section, count}">
{{ section.text }} <span class="badge badge-light ml-1">{{ count }}</span>
</template>
<template #section_body="{item}">
<tr>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.last_activity }}</td>
<td>{{ item.assigned_to }}</td>
<td>
<div class="btn-group">
<a class="btn btn-primary" :href="'/'+ getEventSlug + '/ticket/' + item.id" title="view"
@click.prevent="gotoDetail(item)">
<font-awesome-icon icon="eye"/>
View
</a>
</div>
</td>
</tr>
</template>
</CollapsableCards>
</div> </div>
</template> </template>
<script> <script>
import Table from '@/components/Table';
import Cards from '@/components/Cards'; import Cards from '@/components/Cards';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import EditItem from '@/components/EditItem'; 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 Table from '@/components/Table';
import CollapsableCards from "@/components/CollapsableCards.vue";
export default { export default {
name: 'Tickets', name: 'Tickets',
components: {Lightbox, Table, Cards, Modal, EditItem}, components: {Lightbox, Table, Cards, Modal, EditItem, CollapsableCards},
computed: { computed: {
...mapState(['tickets']), ...mapState(['tickets']),
...mapGetters(['getEventSlug']) ...mapGetters(['stateInfo', 'getEventSlug', 'layout']),
}, },
methods: { methods: {
...mapActions(['loadTickets', 'fetchTicketStates']), ...mapActions(['loadTickets', 'fetchTicketStates']),
gotoDetail(ticket) { gotoDetail(ticket) {
this.$router.push({name: 'ticket', params: {id: ticket.id}}); this.$router.push({name: 'ticket', params: {id: ticket.id}});
},
formatTicket(ticket) {
return {
id: ticket.id,
name: ticket.name,
state: this.stateInfo(ticket.state).text,
stateColor: this.stateInfo(ticket.state).color,
last_activity: ticket.last_activity,
assigned_to: ticket.assigned_to
};
} }
}, },
created() { created() {