add alternative layout for /tickets page

This commit is contained in:
jedi 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"/>
</button>
</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')"
v-if="isItemView()">
<font-awesome-icon icon="plus"/>
@ -108,13 +116,13 @@ export default {
{'title': 'howto engel', 'path': '/howto/'},
]
}),
emits: ['addItemClicked', 'addTicketClicked'],
computed: {
...mapState(['events', 'layout']),
...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions"]),
...mapState(['events']),
...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions", "layout"]),
},
methods: {
...mapActions(['changeEvent', 'changeView', 'searchEventItems']),
...mapMutations(['setLayout', 'logout']),
navigateTo(link) {
if (this.$router.currentRoute.path !== link)
this.$router.push(link);
@ -124,7 +132,12 @@ export default {
},
isTicketView() {
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>

View file

@ -36,7 +36,13 @@ import {
faUser,
faComments,
faArchive,
faMinus, faHourglass, faExclamation, faClipboard,
faMinus,
faHourglass,
faExclamation,
faClipboard,
faTasks,
faAngleRight,
faAngleDown
} from '@fortawesome/free-solid-svg-icons';
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,
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);
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) {
//console.log('update last event', 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;

View file

@ -61,7 +61,6 @@ const store = new Vuex.Store({
state: {
keyIncrement: 0,
events: [],
layout: 'cards',
loadedItems: [],
itemCache: {},
loadedBoxes: [],
@ -80,6 +79,7 @@ const store = new Vuex.Store({
token_expiry: null,
local_loaded: false,
showAddBoxModal: false,
toggle: false,
},
getters: {
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) {
if (!state.local_loaded) {
state.remember = localStorage.getItem('remember') === 'true';
@ -145,9 +154,6 @@ const store = new Vuex.Store({
setItemCache(state, {slug, items}) {
state.itemCache[slug] = items;
},
setLayout(state, layout) {
state.layout = layout;
},
replaceBoxes(state, loadedBoxes) {
state.loadedBoxes = loadedBoxes;
},
@ -223,6 +229,9 @@ const store = new Vuex.Store({
if (router.currentRoute.name !== 'login')
router.push('/login');
},
triggerLayoutChange(state) {
state.toggle = !state.toggle;
}
},
actions: {
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 Modal from '@/components/Modal';
import EditItem from '@/components/EditItem';
import {mapActions, mapState} from 'vuex';
import {mapActions, mapGetters, mapState} from 'vuex';
import Lightbox from '../components/Lightbox';
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
@ -88,7 +88,10 @@ export default {
editingItem: null,
}),
components: {AuthenticatedImage, Lightbox, Table, Cards, Modal, EditItem},
computed: mapState(['loadedItems', 'layout']),
computed: {
...mapState(['loadedItems']),
...mapGetters(['layout']),
},
methods: {
...mapActions(['deleteItem', 'markItemReturned']),
openLightboxModalWith(item) {

View file

@ -6,6 +6,7 @@
:columns="['id', 'name', 'state', 'last_activity', 'assigned_to']"
:items="tickets"
:keyName="'id'"
v-if="layout === 'table'"
>
<template #actions="{ item }">
<div class="btn-group">
@ -19,28 +20,64 @@
</Table>
</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>
</template>
<script>
import Table from '@/components/Table';
import Cards from '@/components/Cards';
import Modal from '@/components/Modal';
import EditItem from '@/components/EditItem';
import {mapActions, mapGetters, mapState} from 'vuex';
import Lightbox from '../components/Lightbox';
import Table from '@/components/Table';
import CollapsableCards from "@/components/CollapsableCards.vue";
export default {
name: 'Tickets',
components: {Lightbox, Table, Cards, Modal, EditItem},
components: {Lightbox, Table, Cards, Modal, EditItem, CollapsableCards},
computed: {
...mapState(['tickets']),
...mapGetters(['getEventSlug'])
...mapGetters(['stateInfo', 'getEventSlug', 'layout']),
},
methods: {
...mapActions(['loadTickets', 'fetchTicketStates']),
gotoDetail(ticket) {
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() {