Compare commits

..

1 commit

Author SHA1 Message Date
343040fa6d add basic view for item history
All checks were successful
/ deploy (push) Successful in 4m46s
/ test (push) Successful in 51s
2024-11-27 19:41:34 +01:00
14 changed files with 199 additions and 158 deletions

View file

@ -1,13 +1,12 @@
from django.urls import re_path
from django.contrib.auth.decorators import permission_required
from rest_framework import routers, viewsets, status
from rest_framework import routers, viewsets
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from inventory.models import Event, Container, Item, Comment
from inventory.serializers import EventSerializer, ContainerSerializer, CommentSerializer, ItemSerializer, \
SearchResultSerializer
from inventory.models import Event, Container, Item
from inventory.serializers import EventSerializer, ContainerSerializer, ItemSerializer, SearchResultSerializer
from base64 import b64decode
@ -23,17 +22,6 @@ class ContainerViewSet(viewsets.ModelViewSet):
queryset = Container.objects.all()
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
def get_queryset(self):
queryset = Item.objects.all()
serializer = self.get_serializer_class()
if hasattr(serializer, 'Meta') and hasattr(serializer.Meta, 'prefetch_related_fields'):
queryset = queryset.prefetch_related(*serializer.Meta.prefetch_related_fields)
return queryset
def filter_items(items, query):
query_tokens = query.split(' ')
for item in items:
@ -61,7 +49,6 @@ def search_items(request, event_slug, query):
@api_view(['GET', 'POST'])
@permission_classes([IsAuthenticated])
def item(request, event_slug):
vs = ItemViewSet()
try:
event = None
if event_slug != 'none':
@ -69,7 +56,7 @@ def item(request, event_slug):
if request.method == 'GET':
if not request.user.has_event_perm(event, 'view_item'):
return Response(status=403)
return Response(ItemSerializer(vs.get_queryset().filter(event=event), many=True).data)
return Response(ItemSerializer(Item.objects.filter(event=event), many=True).data)
elif request.method == 'POST':
if not request.user.has_event_perm(event, 'add_item'):
return Response(status=403)
@ -84,31 +71,12 @@ def item(request, event_slug):
return Response(status=400)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@permission_required('tickets.add_comment', raise_exception=True)
def add_comment(request, event_slug, id):
event = None
if event_slug != 'none':
event = Event.objects.get(slug=event_slug)
item = Item.objects.get(event=event, id=id)
if not request.user.has_event_perm(event, 'view_item'):
return Response(status=403)
if 'comment' not in request.data or request.data['comment'] == '':
return Response({'status': 'error', 'message': 'missing comment'}, status=status.HTTP_400_BAD_REQUEST)
comment = Comment.objects.create(
item=item,
comment=request.data['comment'],
)
return Response(CommentSerializer(comment).data, status=status.HTTP_201_CREATED)
@api_view(['GET', 'PUT', 'DELETE', 'PATCH'])
@permission_classes([IsAuthenticated])
def item_by_id(request, event_slug, id):
try:
event = Event.objects.get(slug=event_slug)
item = ItemViewSet().get_queryset().get(event=event, id=id)
item = Item.objects.get(event=event, id=id)
if request.method == 'GET':
if not request.user.has_event_perm(event, 'view_item'):
return Response(status=403)
@ -149,6 +117,5 @@ urlpatterns = router.urls + [
re_path(r'^(?P<event_slug>[\w-]+)/items/$', item, name='item'),
re_path(r'^(?P<event_slug>[\w-]+)/items/(?P<query>[-A-Za-z0-9+/]*={0,3})/$', search_items, name='search_items'),
re_path(r'^(?P<event_slug>[\w-]+)/item/$', item, name='item'),
re_path(r'^(?P<event_slug>[\w-]+)/item/(?P<id>\d+)/comment/$', add_comment, name='add_comment'),
re_path(r'^(?P<event_slug>[\w-]+)/item/(?P<id>\d+)/$', item_by_id, name='item_by_id'),
]

View file

@ -3,7 +3,7 @@ from rest_framework import serializers
from rest_framework.relations import SlugRelatedField
from files.models import File
from inventory.models import Event, Container, Item, Comment
from inventory.models import Event, Container, Item
from inventory.shared_serializers import BasicItemSerializer
from mail.models import EventAddress
from tickets.shared_serializers import BasicIssueSerializer
@ -38,18 +38,6 @@ class ContainerSerializer(serializers.ModelSerializer):
return len(instance.items)
class CommentSerializer(serializers.ModelSerializer):
def validate(self, attrs):
if 'comment' not in attrs or attrs['comment'] == '':
raise serializers.ValidationError('comment cannot be empty')
return attrs
class Meta:
model = Comment
fields = ('id', 'comment', 'timestamp', 'item')
class ItemSerializer(BasicItemSerializer):
timeline = serializers.SerializerMethodField()
dataImage = serializers.CharField(write_only=True, required=False)
@ -60,7 +48,6 @@ class ItemSerializer(BasicItemSerializer):
fields = ['cid', 'box', 'id', 'description', 'file', 'dataImage', 'returned', 'event', 'related_issues',
'timeline']
read_only_fields = ['id']
prefetch_related_fields = ['comments', 'issue_relation_changes', 'container_history']
def to_internal_value(self, data):
container = None

View file

@ -21,13 +21,7 @@ from tickets.shared_serializers import RelationSerializer
class IssueViewSet(viewsets.ModelViewSet):
serializer_class = IssueSerializer
def get_queryset(self):
queryset = IssueThread.objects.all()
serializer = self.get_serializer_class()
if hasattr(serializer, 'Meta') and hasattr(serializer.Meta, 'prefetch_related_fields'):
queryset = queryset.prefetch_related(*serializer.Meta.prefetch_related_fields)
return queryset
queryset = IssueThread.objects.all().prefetch_related('state_changes', 'comments', 'emails', 'emails__attachments', 'assignments', 'item_relation_changes', 'shipping_vouchers')
class RelationViewSet(viewsets.ModelViewSet):
@ -177,5 +171,5 @@ urlpatterns = ([
re_path(r'^tickets/(?P<pk>\d+)/comment/$', add_comment, name='add_comment'),
re_path(r'^(?P<event_slug>[\w-]+)/tickets/manual/$', manual_ticket, name='manual_ticket'),
re_path(r'^(?P<event_slug>[\w-]+)/tickets/(?P<query>[-A-Za-z0-9+/]*={0,3})/$', search_issues,
name='search_issues'),
name='search_issues'),
] + router.urls)

View file

@ -47,8 +47,6 @@ class IssueSerializer(BasicIssueSerializer):
model = IssueThread
fields = ('id', 'timeline', 'name', 'state', 'assigned_to', 'last_activity', 'uuid', 'related_items', 'event')
read_only_fields = ('id', 'timeline', 'last_activity', 'uuid', 'related_items')
prefetch_related_fields = ['state_changes', 'comments', 'emails', 'emails__attachments', 'assignments',
'item_relation_changes', 'shipping_vouchers']
def to_internal_value(self, data):
ret = super().to_internal_value(data)
@ -65,14 +63,12 @@ class IssueSerializer(BasicIssueSerializer):
@staticmethod
def get_last_activity(self):
try:
last_state_change = max(
[t.timestamp for t in self.state_changes.all()]) if self.state_changes.exists() else None
last_state_change = max([t.timestamp for t in self.state_changes.all()]) if self.state_changes.exists() else None
last_comment = max([t.timestamp for t in self.comments.all()]) if self.comments.exists() else None
last_mail = max([t.timestamp for t in self.emails.all()]) if self.emails.exists() else None
last_assignment = max([t.timestamp for t in self.assignments.all()]) if self.assignments.exists() else None
last_relation = max([t.timestamp for t in
self.item_relation_changes.all()]) if self.item_relation_changes.exists() else None
last_relation = max([t.timestamp for t in self.item_relation_changes.all()]) if self.item_relation_changes.exists() else None
args = [x for x in [last_state_change, last_comment, last_mail, last_assignment, last_relation] if
x is not None]
return max(args)
@ -133,6 +129,7 @@ class IssueSerializer(BasicIssueSerializer):
return sorted(timeline, key=lambda x: x['timestamp'])
class SearchResultSerializer(serializers.Serializer):
search_score = serializers.IntegerField()
item = IssueSerializer()

View file

@ -1,6 +1,5 @@
<template>
<div style="min-height: 100vh; display: flex; flex-direction: column;">
<Lightbox v-if="lightboxHash" :hash="lightboxHash" @close="openLightboxModalWith(null)"/>
<AddItemModal v-if="addItemModalOpen && isLoggedIn" @close="closeAddItemModal()" isModal="true"/>
<AddTicketModal v-if="addTicketModalOpen && isLoggedIn" @close="closeAddTicketModal()" isModal="true"/>
<AddBoxModal v-if="showAddBoxModal && isLoggedIn" @close="closeAddBoxModal()" isModal="true"/>
@ -17,22 +16,20 @@ import {mapState, mapMutations, mapActions, mapGetters} from 'vuex';
import AddTicketModal from "@/components/AddTicketModal.vue";
import AddBoxModal from "@/components/AddBoxModal.vue";
import AddEventModal from "@/components/AddEventModal.vue";
import Lightbox from "@/components/Lightbox.vue";
export default {
name: 'app',
components: {Lightbox, AddBoxModal, AddEventModal, Navbar, AddItemModal, AddTicketModal},
components: {AddBoxModal, AddEventModal, Navbar, AddItemModal, AddTicketModal},
computed: {
...mapState(['loadedItems', 'layout', 'toasts', 'showAddBoxModal', 'showAddEventModal', 'lightboxHash']),
...mapState(['loadedItems', 'layout', 'toasts', 'showAddBoxModal', 'showAddEventModal']),
...mapGetters(['isLoggedIn']),
},
data: () => ({
addItemModalOpen: false,
addTicketModalOpen: false,
addTicketModalOpen: false
}),
methods: {
...mapMutations(['removeToast', 'createToast', 'closeAddBoxModal', 'openAddBoxModal', 'closeAddEventModal',
'openLightboxModalWith']),
...mapMutations(['removeToast', 'createToast', 'closeAddBoxModal', 'openAddBoxModal', 'closeAddEventModal']),
...mapActions(['loadEvents', 'scheduleAfterInit']),
openAddItemModal() {
this.addItemModalOpen = true;
@ -45,7 +42,7 @@ export default {
},
closeAddTicketModal() {
this.addTicketModalOpen = false;
},
}
},
created: function () {
document.title = document.location.hostname;

View file

@ -1,5 +1,6 @@
<template>
<div class="timeline-item-wrapper">
<Lightbox v-if="lightboxHash" :hash="lightboxHash" @close="closeLightboxModal()"/>
<div class="timeline-item-description">
<i class="avatar | small">
<font-awesome-icon icon="user"/>
@ -22,7 +23,7 @@
</div>
<div class="card-footer" v-if="item.attachments.length">
<ul>
<li v-for="attachment in item.attachments" @click="openLightboxModalWith(attachment.hash)">
<li v-for="attachment in item.attachments" @click="openLightboxModalWith(attachment)">
<AuthenticatedImage :src="`/media/2/256/${attachment.hash}/`" :alt="attachment.name"
v-if="attachment.mime_type.startsWith('image/')" cached/>
<AuthenticatedDataLink :href="`/media/2/${attachment.hash}/`" :download="attachment.name"
@ -31,6 +32,26 @@
</ul>
</div>
</div>
<!--button class="show-replies">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-forward"
width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M15 11l4 4l-4 4m4 -4h-11a4 4 0 0 1 0 -8h1"/>
</svg>
Show 3 replies
<span class="avatar-list">
<i class="avatar | small">
<font-awesome-icon icon="user"/>
</i>
<i class="avatar | small">
<font-awesome-icon icon="user"/>
</i>
<i class="avatar | small">
<font-awesome-icon icon="user"/>
</i>
</span>
</button-->
</div>
</template>
@ -38,11 +59,16 @@
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue";
import {mapMutations} from "vuex";
import Lightbox from "@/components/Lightbox.vue";
export default {
name: 'TimelineMail',
components: {AuthenticatedImage, AuthenticatedDataLink},
components: {Lightbox, AuthenticatedImage, AuthenticatedDataLink},
data() {
return {
lightboxHash: null,
}
},
props: {
'item': {
type: Object,
@ -59,7 +85,12 @@ export default {
},
methods: {
...mapMutations(['openLightboxModalWith'])
openLightboxModalWith(attachment) {
this.lightboxHash = attachment.hash;
},
closeLightboxModal() { // Closes the editing modal and discards the edited copy of the item.
this.lightboxHash = null;
},
},
};
</script>

View file

@ -1,5 +1,6 @@
<template>
<div class="timeline-item-wrapper">
<Lightbox v-if="lightboxHash" :hash="lightboxHash" @close="closeLightboxModal()"/>
<div class="timeline-item-description">
<i class="avatar | small">
<font-awesome-icon icon="user"/>
@ -18,7 +19,7 @@
<AuthenticatedImage v-if="item.item.file" cached
:src="`/media/2/256/${item.item.file}/`"
class="d-block card-img-left"
@click="openLightboxModalWith(item.item.file)"
@click="openLightboxModalWith(item.item)"
/>
</div>
<div class="col">
@ -27,10 +28,49 @@
<router-link :to="{name: 'item', params: {id: item.item.id}}">
<h6 class="card-title">{{ item.item.description }}</h6>
</router-link>
<!--div class="row mx-auto mt-2">
<div class="btn-group">
<button class="btn btn-outline-success"
@click.stop="confirm('return Item?') && markItemReturned(item.item)"
title="returned">
<font-awesome-icon icon="check"/>
</button>
<button class="btn btn-outline-secondary" @click.stop="openEditingModalWith(item.item)"
title="edit">
<font-awesome-icon icon="edit"/>
</button>
<button class="btn btn-outline-danger"
@click.stop="confirm('delete Item?') && deleteItem(item.item)"
title="delete">
<font-awesome-icon icon="trash"/>
</button>
</div>
</div>
<p>{{ item }}</p-->
</div>
</div>
</div>
</div>
<!--button class="show-replies">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-forward"
width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M15 11l4 4l-4 4m4 -4h-11a4 4 0 0 1 0 -8h1"/>
</svg>
Show 3 replies
<span class="avatar-list">
<i class="avatar | small">
<font-awesome-icon icon="user"/>
</i>
<i class="avatar | small">
<font-awesome-icon icon="user"/>
</i>
<i class="avatar | small">
<font-awesome-icon icon="user"/>
</i>
</span>
</button-->
</div>
</template>
@ -38,11 +78,16 @@
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue";
import {mapMutations} from "vuex";
import Lightbox from "@/components/Lightbox.vue";
export default {
name: 'TimelineRelatedItem',
components: {AuthenticatedImage, AuthenticatedDataLink},
components: {Lightbox, AuthenticatedImage, AuthenticatedDataLink},
data() {
return {
lightboxHash: null,
}
},
props: {
'item': {
type: Object,
@ -59,8 +104,13 @@ export default {
},
methods: {
...mapMutations(['openLightboxModalWith'])
}
openLightboxModalWith(attachment) {
this.lightboxHash = attachment.hash;
},
closeLightboxModal() { // Closes the editing modal and discards the edited copy of the item.
this.lightboxHash = null;
},
},
};
</script>

View file

@ -19,9 +19,13 @@
<script>
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue";
import Lightbox from "@/components/Lightbox.vue";
export default {
name: 'TimelineRelatedTicket',
components: {},
components: {Lightbox},
props: {
'item': {
type: Object,

View file

@ -1,11 +1,10 @@
import {createRouter, createWebHistory} from 'vue-router'
import store from '@/store';
import Item from "@/views/Item.vue";
import Items from '@/views/Items';
import Boxes from '@/views/Boxes';
import Files from '@/views/Files';
import HowTo from '@/views/HowTo';
import Items from './views/Items';
import Boxes from './views/Boxes';
import Files from './views/Files';
import HowTo from './views/HowTo';
import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue';
import Dashboard from "@/views/admin/Dashboard.vue";

View file

@ -34,7 +34,6 @@ const store = createStore({
expiry: null,
},
lightboxHash: null,
thumbnailCache: {},
fetchedData: {
events: 0,
@ -186,9 +185,6 @@ const store = createStore({
state.groups = groups;
state.fetchedData = {...state.fetchedData, groups: Date.now()};
},
openLightboxModalWith(state, hash) {
state.lightboxHash = hash;
},
openAddBoxModal(state) {
state.showAddBoxModal = true;
},
@ -353,6 +349,7 @@ const store = createStore({
},
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}/`});

View file

@ -8,25 +8,15 @@
<AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`"
class="d-block card-img"
@click="openLightboxModalWith(item.file)"
@click="openLightboxModalWith(item)"
/>
<div class="card-body">
<h6 class="card-subtitle text-secondary">id: {{ item.id }} box: {{ item.box }}</h6>
<h6 class="card-subtitle text-secondary">id: {{ item.id }} box: {{
item.box
}}</h6>
<h6 class="card-title">{{ item.description }}</h6>
</div>
<div class="card-footer">
<InputPhoto
:model="editingItem"
field="file"
:on-capture="storeImage"
/>
<InputString
label="description"
:model="editingItem"
field="description"
:validation-fn="str => str && str.length > 0"
/>
</div>
</div>
</div>
</div>
@ -36,7 +26,7 @@
<h3>Item #{{ item.id }} - {{ item.description }}</h3>
</div>
<Timeline :timeline="item.timeline">
<!--template v-slot:timeline_action1>
<template v-slot:timeline_action1>
<span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="comment"/>
</span>
@ -51,35 +41,47 @@
</AsyncButton>
</div>
</div>
</template-->
</template>
</Timeline>
<div class="card-footer d-flex justify-content-between">
<!--button class="btn btn-secondary mr-2" @click="$router.go(-1)">Back</button-->
<button class="btn btn-secondary mr-2" @click="$router.go(-1)">Back</button>
<div class="btn-group">
<button class="btn btn-outline-success"
@click.stop="confirm('return Item?') && markItemReturned(item)"
title="returned">
<font-awesome-icon icon="check"/>&nbsp;mark&nbsp;returned
<font-awesome-icon icon="check"/>
</button>
<button class="btn btn-outline-secondary" @click.stop="openEditingModalWith(item)"
title="edit">
<font-awesome-icon icon="edit"/>
</button>
<button class="btn btn-outline-danger"
@click.stop="confirm('delete Item?') && deleteItem(item)"
title="delete">
<font-awesome-icon icon="trash"/>&nbsp;delete
<font-awesome-icon icon="trash"/>
</button>
</div>
<InputCombo
label="box"
:model="editingItem"
:model="item"
nameKey="box"
uniqueKey="cid"
:options="boxes"
style="width: auto;"
/>
<button type="button" class="btn btn-success" @click="saveEditingItem()">Save Changes
</button>
{{ editingItem }}
<div class="btn-group">
<select class="form-control" v-model="selected_state">
<option v-for="status in state_options" :value="status.value">{{
status.text
}}
</option>
</select>
<button class="form-control btn btn-success"
@click="changeTicketStatus(item)"
:disabled="(selected_state == item.state)">
Change&nbsp;Status
</button>
</div>
</div>
</div>
</div>
@ -103,29 +105,21 @@
</AsyncLoader>
</template>
<script>
import {mapActions, mapGetters, mapMutations, mapState} from 'vuex';
import {mapActions, mapGetters, mapState} from 'vuex';
import Timeline from "@/components/Timeline.vue";
import ClipboardButton from "@/components/inputs/ClipboardButton.vue";
import AsyncLoader from "@/components/AsyncLoader.vue";
import InputCombo from "@/components/inputs/InputCombo.vue";
import InputString from "@/components/inputs/InputString.vue";
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import InputPhoto from "@/components/inputs/InputPhoto.vue";
import Modal from "@/components/Modal.vue";
import EditItem from "@/components/EditItem.vue";
export default {
name: 'Item',
components: {
EditItem,
Modal, InputPhoto, AuthenticatedImage, InputString, InputCombo, AsyncLoader, ClipboardButton, Timeline
},
components: {AuthenticatedImage, InputString, InputCombo, AsyncLoader, ClipboardButton, Timeline},
data() {
return {
newComment: "",
editingItem: {},
newComment: ""
}
},
computed: {
@ -137,40 +131,33 @@ export default {
return ret ? ret : {};
},
boxes() {
console.log(this.getBoxes);
return this.getBoxes.map(obj => ({cid: obj.cid, box: obj.name}));
}
},
methods: {
...mapActions(['deleteItem', 'markItemReturned', 'updateTicketPartial', 'postComment']),
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit', 'updateItem']),
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']),
...mapActions(['claimShippingVoucher', 'fetchShippingVouchers', 'loadEventItems', 'loadBoxes']),
...mapMutations(['openLightboxModalWith']),
changeTicketStatus(item) {
item.state = this.selected_state;
this.updateTicketPartial({
id: item.id,
state: this.selected_state,
})
},
addCommentAndClear: async function () {
await this.postComment({
id: this.ticket.id,
message: this.newComment
})
this.newComment = "";
},
closeEditingModal() {
this.editingItem = null;
},
async saveEditingItem() { // Saves the edited copy of the item.
await this.updateItem(this.editingItem);
this.editingItem = {...this.item}
},
storeImage(image) {
this.item.dataImage = image;
},
confirm(message) {
return window.confirm(message);
},
}
},
mounted() {
this.scheduleAfterInit(() => [Promise.all([this.loadEventItems(), this.loadBoxes()]).then(() => {
this.selected_state = this.item.state;
this.selected_assignee = this.item.assigned_to
this.editingItem = {...this.item}
})]);
}
};

View file

@ -1,6 +1,19 @@
<template>
<AsyncLoader :loaded="isItemsLoaded">
<div class="container-fluid px-xl-5 mt-3">
<Modal title="Edit Item" v-if="editingItem" @close="closeEditingModal()">
<template #body>
<EditItem
:item="editingItem"
badge="id"
/>
</template>
<template #buttons>
<button type="button" class="btn btn-secondary" @click="closeEditingModal()">Cancel</button>
<button type="button" class="btn btn-success" @click="saveEditingItem()">Save Changes</button>
</template>
</Modal>
<Lightbox v-if="lightboxHash" :hash="lightboxHash" @close="closeLightboxModal()"/>
<div class="row" v-if="layout === 'table'">
<div class="col-xl-8 offset-xl-2">
<Table
@ -34,7 +47,7 @@
:items="getEventItems"
:keyName="'id'"
v-slot="{ item }"
@itemActivated="item => openLightboxModalWith(item.file)"
@itemActivated="showItemDetail"
>
<AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`"
@ -49,7 +62,7 @@
@click.stop="confirm('return Item?') && markItemReturned(item)" title="returned">
<font-awesome-icon icon="check"/>
</button>
<button class="btn btn-outline-secondary" @click.stop="showItemDetail(item)"
<button class="btn btn-outline-secondary" @click.stop="openEditingModalWith(item)"
title="edit">
<font-awesome-icon icon="edit"/>
</button>
@ -71,7 +84,8 @@ import Table from '@/components/Table';
import Cards from '@/components/Cards';
import Modal from '@/components/Modal';
import EditItem from '@/components/EditItem';
import {mapActions, mapGetters, mapMutations} from 'vuex';
import {mapActions, mapGetters, mapState} from 'vuex';
import Lightbox from '../components/Lightbox';
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AsyncLoader from "@/components/AsyncLoader.vue";
import router from "@/router";
@ -82,16 +96,32 @@ export default {
lightboxHash: null,
editingItem: null,
}),
components: {AsyncLoader, AuthenticatedImage, Table, Cards, Modal, EditItem},
components: {AsyncLoader, AuthenticatedImage, Lightbox, Table, Cards, Modal, EditItem},
computed: {
...mapState([]),
...mapGetters(['getEventItems', 'isItemsLoaded', 'layout']),
},
methods: {
...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem', 'scheduleAfterInit']),
...mapMutations(['openLightboxModalWith']),
showItemDetail(item) {
router.push({name: 'item', params: {id: item.id}});
},
openLightboxModalWith(item) {
this.lightboxHash = item.file;
},
closeLightboxModal() { // Closes the editing modal and discards the edited copy of the item.
this.lightboxHash = null;
},
openEditingModalWith(item) { // Opens the editing modal with a copy of the selected item.
this.editingItem = item;
},
closeEditingModal() {
this.editingItem = null;
},
saveEditingItem() { // Saves the edited copy of the item.
this.updateItem(this.editingItem);
this.closeEditingModal();
},
confirm(message) {
return window.confirm(message);
}

View file

@ -105,7 +105,7 @@
<AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`"
class="d-block card-img"
@click="openLightboxModalWith(item.file)"
@click="openLightboxModalWith(item)"
/>
<div class="card-body">
<!--h6 class="card-title text-info"><span class="badge badge-primary">{{ item.relation_status }}</span></--h6-->
@ -126,7 +126,7 @@
</template>
<script>
import {mapActions, mapGetters, mapMutations, mapState} from 'vuex';
import {mapActions, mapGetters, mapState} from 'vuex';
import Timeline from "@/components/Timeline.vue";
import ClipboardButton from "@/components/inputs/ClipboardButton.vue";
import AsyncLoader from "@/components/AsyncLoader.vue";
@ -166,7 +166,6 @@ export default {
...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']),
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']),
...mapActions(['claimShippingVoucher', 'fetchShippingVouchers']),
...mapMutations(['openLightboxModalWith']),
changeTicketStatus() {
this.ticket.state = this.selected_state;
this.updateTicketPartial({

View file

@ -55,22 +55,24 @@
<script>
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";
import AsyncLoader from "@/components/AsyncLoader.vue";
import router from "@/router";
export default {
name: 'Tickets',
components: {AsyncLoader, Table, Cards, CollapsableCards},
components: {AsyncLoader, Lightbox, Table, Cards, Modal, EditItem, CollapsableCards},
computed: {
...mapGetters(['getEventTickets', 'isTicketsLoaded', 'stateInfo', 'getEventSlug', 'layout']),
},
methods: {
...mapActions(['loadTickets', 'fetchTicketStates', 'scheduleAfterInit']),
gotoDetail(ticket) {
router.push({name: 'ticket', params: {id: ticket.id}});
this.$router.push({name: 'ticket', params: {id: ticket.id}});
},
formatTicket(ticket) {
return {