diff --git a/core/authentication/migrations/0004_legacy_user.py b/core/authentication/migrations/0004_legacy_user.py index 1ed0456..76f62dd 100644 --- a/core/authentication/migrations/0004_legacy_user.py +++ b/core/authentication/migrations/0004_legacy_user.py @@ -1,8 +1,6 @@ from django.conf import settings from django.db import migrations -from authentication.models import ExtendedUser - class Migration(migrations.Migration): dependencies = [ @@ -11,6 +9,7 @@ class Migration(migrations.Migration): ] def create_legacy_user(apps, schema_editor): + ExtendedUser = apps.get_model('authentication', 'ExtendedUser') ExtendedUser.objects.create_user(settings.LEGACY_USER_NAME, 'mail@' + settings.MAIL_DOMAIN, settings.LEGACY_USER_PASSWORD) diff --git a/core/inventory/api_v1.py b/core/inventory/api_v1.py index d5a1e29..e9e670d 100644 --- a/core/inventory/api_v1.py +++ b/core/inventory/api_v1.py @@ -6,13 +6,7 @@ from rest_framework.response import Response from files.models import File from inventory.models import Event, Container, Item - - -class EventSerializer(serializers.ModelSerializer): - class Meta: - model = Event - fields = ['eid', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end'] - read_only_fields = ['eid'] +from inventory.serializers import EventSerializer, ContainerSerializer class EventViewSet(viewsets.ModelViewSet): @@ -22,18 +16,6 @@ class EventViewSet(viewsets.ModelViewSet): authentication_classes = [] -class ContainerSerializer(serializers.ModelSerializer): - itemCount = serializers.SerializerMethodField() - - class Meta: - model = Container - fields = ['cid', 'name', 'itemCount'] - read_only_fields = ['cid', 'itemCount'] - - def get_itemCount(self, instance): - return Item.objects.filter(container=instance.cid).count() - - class ContainerViewSet(viewsets.ModelViewSet): serializer_class = ContainerSerializer queryset = Container.objects.all() diff --git a/core/inventory/api_v2.py b/core/inventory/api_v2.py index 5ee00ee..0a34140 100644 --- a/core/inventory/api_v2.py +++ b/core/inventory/api_v2.py @@ -1,21 +1,12 @@ -from datetime import datetime - -from django.urls import path, re_path +from django.urls import path from django.contrib.auth.decorators import permission_required -from rest_framework import routers, viewsets, serializers +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 files.models import File from inventory.models import Event, Container, Item - - -class EventSerializer(serializers.ModelSerializer): - class Meta: - model = Event - fields = ['eid', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end'] - read_only_fields = ['eid'] +from inventory.serializers import EventSerializer, ContainerSerializer, ItemSerializer class EventViewSet(viewsets.ModelViewSet): @@ -24,87 +15,11 @@ class EventViewSet(viewsets.ModelViewSet): permission_classes = [] -class ContainerSerializer(serializers.ModelSerializer): - itemCount = serializers.SerializerMethodField() - - class Meta: - model = Container - fields = ['cid', 'name', 'itemCount'] - read_only_fields = ['cid', 'itemCount'] - - def get_itemCount(self, instance): - return Item.objects.filter(container=instance.cid).count() - - class ContainerViewSet(viewsets.ModelViewSet): serializer_class = ContainerSerializer queryset = Container.objects.all() -class ItemSerializer(serializers.ModelSerializer): - dataImage = serializers.CharField(write_only=True, required=False) - cid = serializers.SerializerMethodField() - box = serializers.SerializerMethodField() - file = serializers.SerializerMethodField() - returned = serializers.SerializerMethodField(required=False) - - class Meta: - model = Item - fields = ['cid', 'box', 'uid', 'description', 'file', 'dataImage', 'returned'] - read_only_fields = ['uid'] - - def get_cid(self, instance): - return instance.container.cid - - def get_box(self, instance): - return instance.container.name - - def get_file(self, instance): - if len(instance.files.all()) > 0: - return instance.files.all().order_by('-created_at')[0].hash - return None - - def get_returned(self, instance): - return instance.returned_at is not None - - def to_internal_value(self, data): - container = None - returned = False - if 'cid' in data: - container = Container.objects.get(cid=data['cid']) - if 'returned' in data: - returned = data['returned'] - internal = super().to_internal_value(data) - if container: - internal['container'] = container - if returned: - internal['returned_at'] = datetime.now() - return internal - - def validate(self, attrs): - return super().validate(attrs) - - def create(self, validated_data): - if 'dataImage' in validated_data: - file = File.objects.create(data=validated_data['dataImage']) - validated_data.pop('dataImage') - item = Item.objects.create(**validated_data) - item.files.set([file]) - return item - return Item.objects.create(**validated_data) - - def update(self, instance, validated_data): - if 'returned' in validated_data: - if validated_data['returned']: - validated_data['returned_at'] = datetime.now() - validated_data.pop('returned') - if 'dataImage' in validated_data: - file = File.objects.create(data=validated_data['dataImage']) - validated_data.pop('dataImage') - instance.files.add(file) - return super().update(instance, validated_data) - - @api_view(['GET']) @permission_classes([IsAuthenticated]) @permission_required('view_item', raise_exception=True) diff --git a/core/inventory/serializers.py b/core/inventory/serializers.py new file mode 100644 index 0000000..fd39c3a --- /dev/null +++ b/core/inventory/serializers.py @@ -0,0 +1,88 @@ +from django.utils import timezone +from rest_framework import serializers + +from files.models import File +from inventory.models import Event, Container, Item + + +class EventSerializer(serializers.ModelSerializer): + class Meta: + model = Event + fields = ['eid', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end'] + read_only_fields = ['eid'] + + +class ContainerSerializer(serializers.ModelSerializer): + itemCount = serializers.SerializerMethodField() + + class Meta: + model = Container + fields = ['cid', 'name', 'itemCount'] + read_only_fields = ['cid', 'itemCount'] + + def get_itemCount(self, instance): + return Item.objects.filter(container=instance.cid).count() + + +class ItemSerializer(serializers.ModelSerializer): + dataImage = serializers.CharField(write_only=True, required=False) + cid = serializers.SerializerMethodField() + box = serializers.SerializerMethodField() + file = serializers.SerializerMethodField() + returned = serializers.SerializerMethodField(required=False) + + class Meta: + model = Item + fields = ['cid', 'box', 'uid', 'description', 'file', 'dataImage', 'returned'] + read_only_fields = ['uid'] + + def get_cid(self, instance): + return instance.container.cid + + def get_box(self, instance): + return instance.container.name + + def get_file(self, instance): + if len(instance.files.all()) > 0: + return instance.files.all().order_by('-created_at')[0].hash + return None + + def get_returned(self, instance): + return instance.returned_at is not None + + def to_internal_value(self, data): + container = None + returned = False + if 'cid' in data: + container = Container.objects.get(cid=data['cid']) + if 'returned' in data: + returned = data['returned'] + internal = super().to_internal_value(data) + if container: + internal['container'] = container + if returned: + internal['returned_at'] = timezone.now() + return internal + + def validate(self, attrs): + return super().validate(attrs) + + def create(self, validated_data): + if 'dataImage' in validated_data: + file = File.objects.create(data=validated_data['dataImage']) + validated_data.pop('dataImage') + item = Item.objects.create(**validated_data) + item.files.set([file]) + return item + return Item.objects.create(**validated_data) + + def update(self, instance, validated_data): + if 'returned' in validated_data: + if validated_data['returned']: + validated_data['returned_at'] = timezone.now() + validated_data.pop('returned') + if 'dataImage' in validated_data: + file = File.objects.create(data=validated_data['dataImage']) + validated_data.pop('dataImage') + instance.files.add(file) + return super().update(instance, validated_data) diff --git a/core/mail/migrations/0003_emailattachment.py b/core/mail/migrations/0003_emailattachment.py index 1d570ba..7796876 100644 --- a/core/mail/migrations/0003_emailattachment.py +++ b/core/mail/migrations/0003_emailattachment.py @@ -3,7 +3,6 @@ from django.db import migrations, models import django.db.models.deletion import files.models -from mail.models import Email from mail.protocol import parse_email_body @@ -24,6 +23,7 @@ class Migration(migrations.Migration): ] def generate_email_attachments(apps, schema_editor): + Email = apps.get_model('mail', 'Email') for email in Email.objects.all(): raw = email.raw if raw is None or raw == '': diff --git a/core/tickets/admin.py b/core/tickets/admin.py index d862811..20cb83f 100644 --- a/core/tickets/admin.py +++ b/core/tickets/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from tickets.models import IssueThread, Comment, StateChange +from tickets.models import IssueThread, Comment, StateChange, Assignment, ShippingVoucher class IssueThreadAdmin(admin.ModelAdmin): @@ -15,6 +15,16 @@ class StateChangeAdmin(admin.ModelAdmin): pass +class AssignmentAdmin(admin.ModelAdmin): + pass + + +class ShippingVouchersAdmin(admin.ModelAdmin): + pass + + admin.site.register(IssueThread, IssueThreadAdmin) admin.site.register(Comment, CommentAdmin) admin.site.register(StateChange, StateChangeAdmin) +admin.site.register(Assignment, AssignmentAdmin) +admin.site.register(ShippingVoucher, ShippingVouchersAdmin) diff --git a/core/tickets/api_v2.py b/core/tickets/api_v2.py index 6557a98..f8f746e 100644 --- a/core/tickets/api_v2.py +++ b/core/tickets/api_v2.py @@ -13,8 +13,8 @@ from core.settings import MAIL_DOMAIN from mail.models import Email from mail.protocol import send_smtp, make_reply, collect_references from notify_sessions.models import SystemEvent -from tickets.models import IssueThread, Comment, STATE_CHOICES -from tickets.serializers import IssueSerializer, CommentSerializer +from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher +from tickets.serializers import IssueSerializer, CommentSerializer, ShippingVoucherSerializer class IssueViewSet(viewsets.ModelViewSet): @@ -22,9 +22,9 @@ class IssueViewSet(viewsets.ModelViewSet): queryset = IssueThread.objects.all() -class CommentViewSet(viewsets.ModelViewSet): - serializer_class = CommentSerializer - queryset = Comment.objects.all() +class ShippingVoucherViewSet(viewsets.ModelViewSet): + serializer_class = ShippingVoucherSerializer + queryset = ShippingVoucher.objects.all() @api_view(['POST']) @@ -118,7 +118,7 @@ def add_comment(request, pk): router = routers.SimpleRouter() router.register(r'tickets', IssueViewSet, basename='issues') -router.register(r'comments', CommentViewSet, basename='comments') +router.register(r'shipping_vouchers', ShippingVoucherViewSet, basename='shipping_vouchers') urlpatterns = ([ re_path(r'^tickets/(?P\d+)/reply/$', reply, name='reply'), diff --git a/core/tickets/migrations/0006_issuethread_uuid.py b/core/tickets/migrations/0006_issuethread_uuid.py index 146809a..6618bbe 100644 --- a/core/tickets/migrations/0006_issuethread_uuid.py +++ b/core/tickets/migrations/0006_issuethread_uuid.py @@ -2,17 +2,15 @@ from django.db import migrations, models -from tickets.models import IssueThread - class Migration(migrations.Migration): - dependencies = [ ('tickets', '0005_remove_issuethread_last_activity'), ] def set_uuid(apps, schema_editor): import uuid + IssueThread = apps.get_model('tickets', 'IssueThread') for issue_thread in IssueThread.objects.all(): issue_thread.uuid = str(uuid.uuid4()) issue_thread.save() diff --git a/core/tickets/migrations/0009_shippingvoucher.py b/core/tickets/migrations/0009_shippingvoucher.py new file mode 100644 index 0000000..a857457 --- /dev/null +++ b/core/tickets/migrations/0009_shippingvoucher.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.7 on 2024-06-23 00:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0008_alter_issuethread_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ShippingVoucher', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('voucher', models.CharField(max_length=255)), + ('type', models.CharField(max_length=255)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('used_at', models.DateTimeField(null=True)), + ('issue_thread', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shipping_vouchers', to='tickets.issuethread')), + ], + ), + ] diff --git a/core/tickets/models.py b/core/tickets/models.py index 6dd42c3..141cf27 100644 --- a/core/tickets/models.py +++ b/core/tickets/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.utils import timezone from django_softdelete.models import SoftDeleteModel from authentication.models import ExtendedUser @@ -116,3 +117,20 @@ class Assignment(models.Model): def __str__(self): return str(self.issue_thread) + ' assigned to ' + self.assigned_to.username + + +class ShippingVoucher(models.Model): + id = models.AutoField(primary_key=True) + issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='shipping_vouchers', null=True) + voucher = models.CharField(max_length=255) + type = models.CharField(max_length=255) + timestamp = models.DateTimeField(auto_now_add=True) + used_at = models.DateTimeField(null=True) + + def __str__(self): + return self.voucher + ' (' + self.type + ')' + + def save(self, *args, **kwargs): + if self.used_at is None and self.issue_thread is not None: + self.used_at = timezone.now() + super().save(*args, **kwargs) diff --git a/core/tickets/serializers.py b/core/tickets/serializers.py index fe3972e..448a902 100644 --- a/core/tickets/serializers.py +++ b/core/tickets/serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers from authentication.models import ExtendedUser from mail.api_v2 import AttachmentSerializer -from tickets.models import IssueThread, Comment, STATE_CHOICES +from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher class CommentSerializer(serializers.ModelSerializer): @@ -28,6 +28,13 @@ class StateSerializer(serializers.Serializer): return obj['value'] +class ShippingVoucherSerializer(serializers.ModelSerializer): + class Meta: + model = ShippingVoucher + fields = ('id', 'voucher', 'type', 'timestamp', 'issue_thread', 'used_at') + read_only_fields = ('id', 'timestamp', 'used_at') + + class IssueSerializer(serializers.ModelSerializer): timeline = serializers.SerializerMethodField() last_activity = serializers.SerializerMethodField() @@ -60,7 +67,10 @@ class IssueSerializer(serializers.ModelSerializer): if self.state_changes.count() > 0 else None last_comment = self.comments.order_by('-timestamp').first().timestamp if self.comments.count() > 0 else None last_mail = self.emails.order_by('-timestamp').first().timestamp if self.emails.count() > 0 else None - args = [x for x in [last_state_change, last_comment, last_mail] if x is not None] + last_assignment = self.assignments.order_by('-timestamp').first().timestamp if \ + self.assignments.count() > 0 else None + args = [x for x in [last_state_change, last_comment, last_mail, last_assignment] if + x is not None] return max(args) except AttributeError: return None @@ -100,6 +110,14 @@ class IssueSerializer(serializers.ModelSerializer): 'timestamp': assignment.timestamp, 'assigned_to': assignment.assigned_to.username, }) + for shipping_voucher in obj.shipping_vouchers.all(): + timeline.append({ + 'type': 'shipping_voucher', + 'id': shipping_voucher.id, + 'timestamp': shipping_voucher.used_at, + 'voucher': shipping_voucher.voucher, + 'voucher_type': shipping_voucher.type, + }) return sorted(timeline, key=lambda x: x['timestamp']) def get_queryset(self): diff --git a/core/tickets/tests/v2/test_shipping_vouchers.py b/core/tickets/tests/v2/test_shipping_vouchers.py new file mode 100644 index 0000000..45fa245 --- /dev/null +++ b/core/tickets/tests/v2/test_shipping_vouchers.py @@ -0,0 +1,41 @@ +from datetime import datetime, timedelta + +from django.test import TestCase, Client + +from authentication.models import ExtendedUser +from mail.models import Email, EmailAttachment +from tickets.models import IssueThread, StateChange, Comment, ShippingVoucher +from django.contrib.auth.models import Permission +from knox.models import AuthToken + + +class ShippingVoucherApiTest(TestCase): + + def setUp(self): + super().setUp() + self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') + self.user.user_permissions.add(*Permission.objects.all()) + self.user.save() + self.token = AuthToken.objects.create(user=self.user) + self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) + + def test_issues_empty(self): + response = self.client.get('/api/2/shipping_vouchers/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), []) + + def test_issues_list(self): + ShippingVoucher.objects.create(voucher='1234', type='2kg-eu') + response = self.client.get('/api/2/shipping_vouchers/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()[0]['voucher'], '1234') + self.assertEqual(response.json()[0]['used_at'], None) + self.assertEqual(response.json()[0]['issue_thread'], None) + self.assertEqual(response.json()[0]['type'], '2kg-eu') + + def test_issues_create(self): + response = self.client.post('/api/2/shipping_vouchers/', {'voucher': '1234', 'type': '2kg-eu'}) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json()['voucher'], '1234') + self.assertEqual(response.json()['used_at'], None) + self.assertEqual(response.json()['issue_thread'], None) diff --git a/core/tickets/tests/v2/test_tickets.py b/core/tickets/tests/v2/test_tickets.py index 6b7a048..8223506 100644 --- a/core/tickets/tests/v2/test_tickets.py +++ b/core/tickets/tests/v2/test_tickets.py @@ -247,16 +247,6 @@ class IssueApiTest(TestCase): self.assertEqual(timeline[1]['subject'], 'test issue') self.assertEqual(timeline[1]['body'], 'test') - def test_post_comment(self): - issue = IssueThread.objects.create( - name="test issue", - ) - response = self.client.post('/api/2/comments/', {'comment': 'test', 'issue_thread': issue.id}) - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json()['comment'], 'test') - self.assertEqual(response.json()['issue_thread'], issue.id) - self.assertEqual(response.json()['timestamp'], response.json()['timestamp']) - def test_post_comment_altenative(self): issue = IssueThread.objects.create( name="test issue", diff --git a/web/src/components/Timeline.vue b/web/src/components/Timeline.vue index b53128c..7df89fa 100644 --- a/web/src/components/Timeline.vue +++ b/web/src/components/Timeline.vue @@ -8,12 +8,19 @@ - + + + + + + + @@ -21,6 +28,8 @@ + +

{{ item }}

  • @@ -66,10 +75,15 @@ import TimelineComment from "@/components/TimelineComment.vue"; import TimelineStateChange from "@/components/TimelineStateChange.vue"; import {mapGetters} from "vuex"; import TimelineAssignment from "@/components/TimelineAssignment.vue"; +import TimelineRelatedItem from "@/components/TimelineRelatedItem.vue"; +import TimelineShippingVoucher from "@/components/TimelineShippingVoucher.vue"; export default { name: 'Timeline', - components: {TimelineAssignment, TimelineStateChange, TimelineComment, TimelineMail}, + components: { + TimelineShippingVoucher, + TimelineRelatedItem, TimelineAssignment, TimelineStateChange, TimelineComment, TimelineMail + }, props: { timeline: { type: Array, diff --git a/web/src/components/TimelineRelatedItem.vue b/web/src/components/TimelineRelatedItem.vue new file mode 100644 index 0000000..8d24fc9 --- /dev/null +++ b/web/src/components/TimelineRelatedItem.vue @@ -0,0 +1,252 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/TimelineShippingVoucher.vue b/web/src/components/TimelineShippingVoucher.vue new file mode 100644 index 0000000..fb7c575 --- /dev/null +++ b/web/src/components/TimelineShippingVoucher.vue @@ -0,0 +1,92 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/TimelineStateChange.vue b/web/src/components/TimelineStateChange.vue index eab7c41..d771b9e 100644 --- a/web/src/components/TimelineStateChange.vue +++ b/web/src/components/TimelineStateChange.vue @@ -25,7 +25,15 @@ export default { computed: { ...mapState(['state_options']), lookupState: function () { - return this.state_options.find(state => state.value === this.item.state); + try { + if (this.item.state) + return this.state_options.find(state => state.value === this.item.state); + } catch (e) { + } + return { + text: 'Unknown', + value: 'unknown' + }; }, colorLookup: function () { if (this.item.state.startsWith('closed_')) { diff --git a/web/src/main.js b/web/src/main.js index 865436d..f6fe706 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -39,13 +39,15 @@ import { faClipboard, faTasks, faAngleRight, - faAngleDown + faAngleDown, + faTruck, + faObjectGroup } from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'; 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, faTasks, faAngleDown, faAngleRight); + faArchive, faMinus, faExclamation, faHourglass, faClipboard, faTasks, faAngleDown, faAngleRight, faTruck, faObjectGroup); const app = createApp(App).use(store).use(router); diff --git a/web/src/router.js b/web/src/router.js index 3490bc3..7d756e8 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -15,6 +15,7 @@ import Empty from "@/views/Empty.vue"; import Events from "@/views/admin/Events.vue"; import AccessControl from "@/views/admin/AccessControl.vue"; import {default as BoxesAdmin} from "@/views/admin/Boxes.vue" +import Shipping from "@/views/admin/Shipping.vue"; const routes = [ {path: '/', redirect: '/37C3/items', meta: {requiresAuth: false}}, @@ -69,6 +70,10 @@ const routes = [ path: 'boxes/', name: 'admin_boxes', component: BoxesAdmin, meta: {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}}, diff --git a/web/src/store.js b/web/src/store.js index 7ee6614..e07c9e0 100644 --- a/web/src/store.js +++ b/web/src/store.js @@ -6,7 +6,6 @@ import * as utf8 from 'utf8'; import {ticketStateColorLookup, ticketStateIconLookup, http} from "@/utils"; import sharedStatePlugin from "@/shared-state-plugin"; import persistentStatePlugin from "@/persistent-state-plugin"; -import {triggerRef} from "vue"; const store = createStore({ state: { @@ -20,6 +19,8 @@ const store = createStore({ users: [], groups: [], state_options: [], + shippingVouchers: [], + lastEvent: '37C3', lastUsed: {}, remember: false, @@ -40,12 +41,22 @@ const store = createStore({ users: 0, groups: 0, states: 0, + shippingVouchers: 0, }, persistent_loaded: false, shared_loaded: false, afterInitHandlers: [], 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: { 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) => { if (router.currentRoute.value.query.layout) return router.currentRoute.value.query.layout; @@ -139,7 +156,6 @@ const store = createStore({ updateTicket(state, updatedTicket) { const ticket = state.tickets.filter(({id}) => id === updatedTicket.id)[0]; Object.assign(ticket, updatedTicket); - //triggerRef(state.tickets); state.tickets = [...state.tickets]; }, replaceUsers(state, users) { @@ -198,6 +214,10 @@ const store = createStore({ 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}) { @@ -412,6 +432,33 @@ const store = createStore({ async updateTicketPartial({commit, state}, {id, ...ticket}) { const {data, success} = await http.patch(`/2/tickets/${id}/`, ticket, state.user.token); 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: [ @@ -427,7 +474,7 @@ const store = createStore({ ] }), sharedStatePlugin({ - debug: true, + debug: false, isLoadedKey: "shared_loaded", clearingMutation: "logout", afterInit: "afterSharedInit", @@ -440,6 +487,7 @@ const store = createStore({ "groups", "loadedBoxes", "loadedItems", + "shippingVouchers", ], watch: [ "test", @@ -450,6 +498,7 @@ const store = createStore({ "groups", "loadedBoxes", "loadedItems", + "shippingVouchers", ], mutations: [ //"replaceTickets", diff --git a/web/src/views/Ticket.vue b/web/src/views/Ticket.vue index b4d1e0e..e92fb8e 100644 --- a/web/src/views/Ticket.vue +++ b/web/src/views/Ticket.vue @@ -13,10 +13,6 @@ Delete - - - Copy DHL contact to clipboard -
    + + + +
    + @@ -41,15 +55,21 @@ + + \ No newline at end of file