import {createStore} from 'vuex'; import router from './router'; import * as base64 from 'base-64'; import * as utf8 from 'utf8'; import {ticketStateColorLookup, ticketStateIconLookup, http, http_session} from "@/utils"; import sharedStatePlugin from "@/shared-state-plugin"; import persistentStatePlugin from "@/persistent-state-plugin"; const store = createStore({ state: { keyIncrement: 0, events: [], items: [], loadedBoxes: [], toasts: [], users: [], groups: [], state_options: [], shippingVouchers: [], loadedItems: {}, loadedTickets: {}, lastEvent: 'all', lastUsed: {}, searchQuery: '', remember: false, user: { username: null, password: null, permissions: [], token: null, expiry: null, }, thumbnailCache: {}, fetchedData: { events: 0, items: 0, boxes: 0, tickets: 0, users: 0, groups: 0, states: 0, shippingVouchers: 0, }, persistent_loaded: false, shared_loaded: false, afterInitHandlers: [], showAddBoxModal: false, showAddEventModal: 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: { route: state => router.currentRoute.value, session: state => http_session(state.user.token), getEventSlug: state => router.currentRoute.value.params.event ? router.currentRoute.value.params.event : state.lastEvent, getAllItems: state => Object.values(state.loadedItems).flat(), getAllTickets: state => Object.values(state.loadedTickets).flat(), getEventItems: (state, getters) => getters.getEventSlug === 'all' ? getters.getAllItems : getters.getAllItems.filter(t => t.event === getters.getEventSlug || (t.event == null && getters.getEventSlug === 'none')), getEventTickets: (state, getters) => getters.getEventSlug === 'all' ? getters.getAllTickets : getters.getAllTickets.filter(t => t.event === getters.getEventSlug || (t.event == null && getters.getEventSlug === 'none')), isItemsLoaded: (state, getters) => (getters.getEventSlug === 'all' || getters.getEventSlug === 'none') ? !!state.loadedItems : Object.keys(state.loadedItems).includes(getters.getEventSlug), isTicketsLoaded: (state, getters) => (getters.getEventSlug === 'all' || getters.getEventSlug === 'none') ? !!state.loadedTickets : Object.keys(state.loadedTickets).includes(getters.getEventSlug), getActiveView: state => router.currentRoute.value.name || 'items', getFilters: state => router.currentRoute.value.query, getBoxes: state => state.loadedBoxes, checkPermission: state => (event, perm) => state.user.permissions && (state.user.permissions.includes(`${event}:${perm}`) || state.user.permissions.includes(`*:${perm}`)), hasPermissions: state => state.user.permissions && state.user.permissions.length > 0, activeUser: state => state.user.username || 'anonymous', stateInfo: state => (slug) => { const obj = state.state_options.filter((s) => s.value === slug)[0]; if (obj) { return { color: ticketStateColorLookup(obj.value), icon: ticketStateIconLookup(obj.value), slug: obj.value, text: obj.text, } } else { return { color: 'danger', icon: 'exclamation', slug: slug, text: 'Unknown' } } }, 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) => { if (router.currentRoute.value.query.layout) return router.currentRoute.value.query.layout; if (getters.getActiveView === 'items') return 'cards'; if (getters.getActiveView === 'tickets') return 'tasks'; }, isLoggedIn(state) { return state.user && state.user.username !== null && state.user.token !== null; }, getThumbnail: (state) => (url) => { if (!url) return null; if (!(url in state.thumbnailCache)) return null; return state.thumbnailCache[url]; }, }, mutations: { updateLastUsed(state, diff) { state.lastUsed = {...state.lastUsed, ...diff}; }, updateLastEvent(state, slug) { state.lastEvent = slug; }, replaceEvents(state, events) { state.events = events; state.fetchedData = {...state.fetchedData, events: Date.now()}; }, replaceTicketStates(state, states) { state.state_options = states; state.fetchedData = {...state.fetchedData, states: Date.now()}; }, changeView(state, {view, slug}) { router.push({path: `/${slug}/${view}`}); }, replaceBoxes(state, loadedBoxes) { state.loadedBoxes = loadedBoxes; state.fetchedData = {...state.fetchedData, boxes: Date.now()}; }, setItems(state, {slug, items}) { state.loadedItems[slug] = items; state.loadedItems = {...state.loadedItems}; console.log(state.loadedItems) }, replaceItems(state, items) { const groups = Object.groupBy(items, i => i.event ? i.event : 'none') for (const [key, value] of Object.entries(groups)) state.loadedItems[key] = value; state.loadedItems = {...state.loadedItems}; console.log(state.loadedItems) }, updateItem(state, updatedItem) { const item = state.loadedItems[updatedItem.event?updatedItem.event:'none'].filter( ({uid}) => uid === updatedItem.uid)[0]; Object.assign(item, updatedItem); }, removeItem(state, item) { state.loadedItems[item.event?item.event:'none'] = state.loadedItems[item.event].filter(it => it !== item); }, appendItem(state, item) { state.loadedItems[item.event?item.event:'none'].push(item); }, setTickets(state, {slug, tickets}) { state.loadedTickets[slug] = tickets; state.loadedTickets = {...state.loadedTickets}; console.log(state.loadedTickets) }, replaceTickets(state, tickets) { const groups = Object.groupBy(tickets, t => t.event ? t.event : 'none') for (const [key, value] of Object.entries(groups)) state.loadedTickets[key] = value; state.loadedTickets = {...state.loadedTickets}; console.log(state.loadedTickets) }, updateTicket(state, updatedTicket) { const ticket = state.loadedTickets[updatedTicket.event?updatedTicket.event:'none'].filter( ({id}) => id === updatedTicket.id)[0]; Object.assign(ticket, updatedTicket); state.loadedTickets = {...state.loadedTickets}; }, replaceUsers(state, users) { state.users = users; state.fetchedData = {...state.fetchedData, users: Date.now()}; }, replaceGroups(state, groups) { state.groups = groups; state.fetchedData = {...state.fetchedData, groups: Date.now()}; }, openAddBoxModal(state) { state.showAddBoxModal = true; }, closeAddBoxModal(state) { state.showAddBoxModal = false; }, openAddEventModal(state) { state.showAddEventModal = true; }, closeAddEventModal(state) { state.showAddEventModal = false; }, createToast(state, {title, message, color}) { var toast = {title, message, color, key: state.keyIncrement} state.toasts.push(toast); state.keyIncrement += 1; return toast; }, removeToast(state, key) { state.toasts = state.toasts.filter(toast => toast.key !== key); }, setRemember(state, remember) { state.remember = remember; }, setUser(state, user) { state.user.username = user; }, setPassword(state, password) { state.user.password = password; }, setPermissions(state, permissions) { state.user.permissions = permissions; }, setToken(state, {token, expiry}) { const user = {...state.user}; user.token = token; user.expiry = expiry; state.user = user; }, setUserInfo(state, user) { state.user = user; }, logout(state) { const user = {...state.user}; user.user = null; user.password = null; user.token = null; user.expiry = null; user.permissions = null; state.user = user; }, setThumbnail(state, {url, data}) { state.thumbnailCache[url] = data; }, setShippingVouchers(state, codes) { state.shippingVouchers = codes; state.fetchedData = {...state.fetchedData, shippingVouchers: Date.now()}; }, }, actions: { async login({commit}, {username, password, remember}) { commit('setRemember', remember); try { const data = await fetch('/api/2/login/', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username: username, password: password}), credentials: 'omit' }).then(r => r.json()) if (data && data.token) { const {data: {permissions}} = await http.get('/2/self/', data.token); commit('setUserInfo', {...data, permissions, username, password}); return true; } else { return false; } } catch (e) { console.error(e); return false; } }, async reloadToken({commit, state}) { try { if (state.user.username && state.user.password) { const data = await fetch('/api/2/login/', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username: state.user.username, password: state.user.password}), credentials: 'omit' }).then(r => r.json()).catch(e => console.error(e)) if (data && data.token) { commit('setToken', data); return true; } } } catch (e) { console.error(e); } //credentials failed, logout store.commit('logout'); }, //async verifyToken({commit, state}) { async afterLogin({dispatch, state}) { let promises = []; promises.push(dispatch('loadBoxes')); promises.push(dispatch('fetchTicketStates')); promises.push(dispatch('loadEventItems')); promises.push(dispatch('loadTickets')); if (!state.user.permissions) { promises.push(dispatch('loadUserInfo')); } await Promise.all(promises); }, async afterSharedInit({dispatch, state}) { const handlers = state.afterInitHandlers; state.afterInitHandlers = []; await Promise.all(handlers.map(h => h()).flat()); }, scheduleAfterInit({dispatch, state}, handler) { if (state.shared_loaded) { Promise.all(handler()).then(() => { }); } else { state.afterInitHandlers.push(handler); } }, async fetchImage({state}, url) { return await fetch(url, {headers: {'Authorization': `Token ${state.user.token}`}}); }, async loadUserInfo({commit, getters}) { const {data, success} = await getters.session.get('/2/self/'); commit('setPermissions', data.permissions); }, async loadEvents({commit, state, getters}) { if (!state.user.token) return; if (state.fetchedData.events > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await getters.session.get('/2/events/'); if (data && success) commit('replaceEvents', data); }, async createEvent({commit, dispatch, state, getters}, event) { const {data, success} = await getters.session.post('/2/events/', event); if (data && success) commit('replaceEvents', [...state.events, data]); }, async deleteEvent({commit, dispatch, state, getters}, event_id) { const {data, success} = await getters.session.delete(`/2/events/${event_id}/`); if (success) { await dispatch('loadEvents') commit('replaceEvents', [...state.events.filter(e => e.eid !== event_id)]) } }, async fetchTicketStates({commit, state, getters}) { if (!state.user.token) return; if (state.fetchedData.states > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await getters.session.get('/2/tickets/states/'); if (data && success) commit('replaceTicketStates', data); }, async changeEvent({dispatch, getters, commit}, eventName) { await router.push({path: `/${eventName.slug}/${getters.getActiveView}/`}); //dispatch('loadEventItems'); }, async changeView({getters}, link) { await router.push({path: `/${getters.getEventSlug}/${link.path}/`}); }, async showBoxContent({getters}, box) { await router.push({path: `/${getters.getEventSlug}/items/`, query: {box}}); }, async loadEventItems({commit, getters, state}) { if (!state.user.token) return; if (state.fetchedData.items > Date.now() - 1000 * 60 * 60 * 24) return; try { const slug = getters.getEventSlug; const {data, success} = await getters.session.get(`/2/${slug}/items/`); if (data && success) { commit('setItems', {slug, items: data}); } } catch (e) { console.error("Error loading items"); } }, async searchEventItems({commit, getters, state}, query) { const encoded_query = base64.encode(utf8.encode(query)); const slug = getters.getEventSlug; const { data, success } = await getters.session.get(`/2/${slug}/items/${encoded_query}/`); if (data && success) { commit('setItems', {slug, items: data}); } }, async loadBoxes({commit, state, getters}) { if (!state.user.token) return; if (state.fetchedData.boxes > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await getters.session.get('/2/boxes/'); if (data && success) commit('replaceBoxes', data); }, async createBox({commit, dispatch, state, getters}, box) { const {data, success} = await getters.session.post('/2/boxes/', box); commit('replaceBoxes', data); dispatch('loadBoxes').then(() => { commit('closeAddBoxModal'); }); }, async deleteBox({commit, dispatch, state, getters}, box_id) { await getters.session.delete(`/2/boxes/${box_id}/`); dispatch('loadBoxes'); }, async updateItem({commit, getters, state}, item) { const { data, success } = await getters.session.put(`/2/${getters.getEventSlug}/item/${item.uid}/`, item); commit('updateItem', data); }, async markItemReturned({commit, getters, state}, item) { await getters.session.patch(`/2/${getters.getEventSlug}/item/${item.uid}/`, {returned: true}, state.user.token); commit('removeItem', item); }, async deleteItem({commit, getters, state}, item) { await getters.session.delete(`/2/${getters.getEventSlug}/item/${item.uid}/`, item); commit('removeItem', item); }, async postItem({commit, getters, state}, item) { commit('updateLastUsed', {box: item.box, cid: item.cid}); const {data, success} = await getters.session.post(`/2/${getters.getEventSlug}/item/`, item); commit('appendItem', data); }, async loadTickets({commit, state, getters}) { if (!state.user.token) return; //if (state.fetchedData.tickets > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await getters.session.get('/2/tickets/'); if (data && success) commit('replaceTickets', data); }, async searchEventTickets({commit, getters, state}, query) { const encoded_query = base64.encode(utf8.encode(query)); const { data, success } = await getters.session.get(`/2/${getters.getEventSlug}/tickets/${encoded_query}/`); if (data && success) commit('replaceTickets', data); }, async sendMail({commit, dispatch, state, getters}, {id, message}) { const {data, success} = await getters.session.post(`/2/tickets/${id}/reply/`, {message}, state.user.token); if (data && success) { state.fetchedData.tickets = 0; await dispatch('loadTickets'); } }, async postManualTicket({commit, dispatch, state, getters}, {sender, message, title,}) { const {data, success} = await getters.session.post(`/2/tickets/manual/`, { name: title, sender, body: message, recipient: 'mail@c3lf.de' }); await dispatch('loadTickets'); }, async postComment({commit, dispatch, state, getters}, {id, message}) { const {data, success} = await getters.session.post(`/2/tickets/${id}/comment/`, {comment: message}); if (data && success) { state.fetchedData.tickets = 0; await dispatch('loadTickets'); } }, async loadUsers({commit, state, getters}) { if (!state.user.token) return; if (state.fetchedData.users > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await getters.session.get('/2/users/'); if (data && success) commit('replaceUsers', data); }, async loadGroups({commit, state, getters}) { if (!state.user.token) return; if (state.fetchedData.groups > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await getters.session.get('/2/groups/'); if (data && success) commit('replaceGroups', data); }, async updateTicket({commit, state, getters}, ticket) { const {data, success} = await getters.session.put(`/2/tickets/${ticket.id}/`, ticket); commit('updateTicket', data); }, async updateTicketPartial({commit, state, getters}, {id, ...ticket}) { const {data, success} = await getters.session.patch(`/2/tickets/${id}/`, ticket); commit('updateTicket', data); }, async fetchShippingVouchers({commit, state, getters}) { if (!state.user.token) return; if (state.fetchedData.shippingVouchers > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await getters.session.get('/2/shipping_vouchers/'); if (data && success) { commit('setShippingVouchers', data); } }, async createShippingVoucher({dispatch, state, getters}, code) { const {data, success} = await getters.session.post('/2/shipping_vouchers/', code); if (data && success) { state.fetchedData.shippingVouchers = 0; dispatch('fetchShippingVouchers'); } }, async claimShippingVoucher({dispatch, state, getters}, {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 getters.session.patch(`/2/shipping_vouchers/${id}/`, {issue_thread: ticket}); if (data && success) { state.fetchedData.shippingVouchers = 0; state.fetchedData.tickets = 0; await Promise.all([dispatch('loadTickets'), dispatch('fetchShippingVouchers')]); } } }, plugins: [persistentStatePlugin({ // TODO change remember to some kind of enable field prefix: "lf_", debug: false, isLoadedKey: "persistent_loaded", state: ["remember", "user", "events", "lastUsed",] }), sharedStatePlugin({ debug: false, isLoadedKey: "shared_loaded", clearingMutation: "logout", afterInit: "afterSharedInit", state: ["test", "state_options", "fetchedData", "loadedItems", "users", "groups", "loadedBoxes", "loadedTickets", "shippingVouchers",], watch: ["test", "state_options", "fetchedData", "loadedItems", "users", "groups", "loadedBoxes", "loadedTickets", "shippingVouchers",], mutations: [//"replaceTickets", ], }),], }); store.watch((state) => state.user, (user) => { if (store.getters.isLoggedIn) { if (router.currentRoute.value.name === 'login' && router.currentRoute.value.query.redirect) { router.push(router.currentRoute.value.query.redirect); } else if (router.currentRoute.value.name === 'login') { router.push('/'); } } else { if (router.currentRoute.value.name !== 'login') { router.push({ name: 'login', query: {redirect: router.currentRoute.value.fullPath}, }); } } }); export default store;