Compare commits

..

No commits in common. "1a3aab4d418b2e4e7fd314e69cf04f768b36bae6" and "4f93a3654652577a7f8f91d5b20a613d31c01083" have entirely different histories.

20 changed files with 533 additions and 596 deletions

View file

@ -17,12 +17,12 @@ jobs:
- name: Install dependencies - name: Install dependencies
working-directory: core working-directory: core
run: pip3 install -r requirements.dev.txt run: pip3 install -r requirements.dev.txt
- name: Run django tests with coverage - name: Run django tests
working-directory: core working-directory: core
run: coverage run manage.py test run: python3 manage.py test
- name: Evaluate coverage - name: Run django coverage
working-directory: core working-directory: core
run: coverage report run: coverage manage.py test
deploy: deploy:
needs: [ test ] needs: [ test ]

View file

@ -9,9 +9,6 @@ omit =
*/tests/* */tests/*
*/migrations/* */migrations/*
core/asgi.py core/asgi.py
core/globals.py core/wsgi.py
core/settings.py core/settings.py
mail/socket.py
manage.py manage.py
server.py
helper.py

View file

@ -92,7 +92,7 @@ class Comment(models.Model):
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return str(self.item) + ' comment #' + str(self.id) return str(self.issue_thread) + ' comment #' + str(self.id)
class Event(models.Model): class Event(models.Model):

View file

@ -105,14 +105,6 @@ class ItemSerializer(BasicItemSerializer):
'timestamp': relation.timestamp, 'timestamp': relation.timestamp,
'issue_thread': BasicIssueSerializer(relation.issue_thread).data, 'issue_thread': BasicIssueSerializer(relation.issue_thread).data,
}) })
for placement in (obj.container_history.all()):
timeline.append({
'type': 'placement',
'id': placement.id,
'timestamp': placement.timestamp,
'cid': placement.container.id,
'box': placement.container.name
})
return sorted(timeline, key=lambda x: x['timestamp']) return sorted(timeline, key=lambda x: x['timestamp'])

View file

@ -7,7 +7,7 @@ from knox.models import AuthToken
from authentication.models import ExtendedUser from authentication.models import ExtendedUser
from files.models import File from files.models import File
from inventory.models import Event, Container, Item, Comment, ItemPlacement from inventory.models import Event, Container, Item, Comment
from base64 import b64encode from base64 import b64encode
@ -19,8 +19,7 @@ class ItemTestCase(TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.event = Event.objects.create(slug='EVENT', name='Event') self.event = Event.objects.create(slug='EVENT', name='Event')
self.box1 = Container.objects.create(name='BOX1') self.box = Container.objects.create(name='BOX')
self.box2 = Container.objects.create(name='BOX2')
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.user_permissions.add(*Permission.objects.all()) self.user.user_permissions.add(*Permission.objects.all())
self.token = AuthToken.objects.create(user=self.user) self.token = AuthToken.objects.create(user=self.user)
@ -37,7 +36,7 @@ class ItemTestCase(TestCase):
def test_members_and_timeline(self): def test_members_and_timeline(self):
now = datetime.now() now = datetime.now()
item = Item.objects.create(container=self.box1, event=self.event, description='1') item = Item.objects.create(container=self.box, event=self.event, description='1')
comment = Comment.objects.create( comment = Comment.objects.create(
item=item, item=item,
comment="test", comment="test",
@ -45,12 +44,7 @@ class ItemTestCase(TestCase):
) )
match = ItemRelation.objects.create( match = ItemRelation.objects.create(
issue_thread=self.issue, issue_thread=self.issue,
item=item, item = item,
timestamp=now + timedelta(seconds=4),
)
placement = ItemPlacement.objects.create(
container=self.box2,
item=item,
timestamp=now + timedelta(seconds=5), timestamp=now + timedelta(seconds=5),
) )
response = self.client.get(f'/api/2/{self.event.slug}/item/') response = self.client.get(f'/api/2/{self.event.slug}/item/')
@ -58,34 +52,25 @@ class ItemTestCase(TestCase):
self.assertEqual(len(response.json()), 1) self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['id'], item.id) self.assertEqual(response.json()[0]['id'], item.id)
self.assertEqual(response.json()[0]['description'], '1') self.assertEqual(response.json()[0]['description'], '1')
self.assertEqual(response.json()[0]['box'], 'BOX2') self.assertEqual(response.json()[0]['box'], 'BOX')
self.assertEqual(response.json()[0]['cid'], self.box2.id) self.assertEqual(response.json()[0]['cid'], self.box.id)
self.assertEqual(response.json()[0]['file'], None) self.assertEqual(response.json()[0]['file'], None)
self.assertEqual(response.json()[0]['returned'], False) self.assertEqual(response.json()[0]['returned'], False)
self.assertEqual(response.json()[0]['event'], self.event.slug) self.assertEqual(response.json()[0]['event'], self.event.slug)
self.assertEqual(len(response.json()[0]['timeline']), 4) self.assertEqual(len(response.json()[0]['timeline']), 2)
self.assertEqual(response.json()[0]['timeline'][0]['type'], 'placement') self.assertEqual(response.json()[0]['timeline'][0]['type'], 'comment')
self.assertEqual(response.json()[0]['timeline'][1]['type'], 'comment') self.assertEqual(response.json()[0]['timeline'][1]['type'], 'issue_relation')
self.assertEqual(response.json()[0]['timeline'][2]['type'], 'issue_relation') self.assertEqual(response.json()[0]['timeline'][0]['id'], comment.id)
self.assertEqual(response.json()[0]['timeline'][3]['type'], 'placement') self.assertEqual(response.json()[0]['timeline'][1]['id'], match.id)
self.assertEqual(response.json()[0]['timeline'][1]['id'], comment.id) self.assertEqual(response.json()[0]['timeline'][0]['comment'], 'test')
self.assertEqual(response.json()[0]['timeline'][2]['id'], match.id) self.assertEqual(response.json()[0]['timeline'][0]['timestamp'],
self.assertEqual(response.json()[0]['timeline'][3]['id'], placement.id)
self.assertEqual(response.json()[0]['timeline'][0]['box'], 'BOX1')
self.assertEqual(response.json()[0]['timeline'][0]['cid'], self.box1.id)
self.assertEqual(response.json()[0]['timeline'][1]['comment'], 'test')
self.assertEqual(response.json()[0]['timeline'][1]['timestamp'],
comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(response.json()[0]['timeline'][2]['status'], 'possible') self.assertEqual(response.json()[0]['timeline'][1]['status'], 'possible')
self.assertEqual(response.json()[0]['timeline'][2]['timestamp'], self.assertEqual(response.json()[0]['timeline'][1]['timestamp'],
match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(response.json()[0]['timeline'][2]['issue_thread']['name'], "test issue") self.assertEqual(response.json()[0]['timeline'][1]['issue_thread']['name'], "test issue")
self.assertEqual(response.json()[0]['timeline'][2]['issue_thread']['event'], "EVENT") self.assertEqual(response.json()[0]['timeline'][1]['issue_thread']['event'], "EVENT")
self.assertEqual(response.json()[0]['timeline'][2]['issue_thread']['state'], "pending_new") self.assertEqual(response.json()[0]['timeline'][1]['issue_thread']['state'], "pending_new")
self.assertEqual(response.json()[0]['timeline'][3]['box'], 'BOX2')
self.assertEqual(response.json()[0]['timeline'][3]['cid'], self.box2.id)
self.assertEqual(response.json()[0]['timeline'][3]['timestamp'],
placement.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(len(response.json()[0]['related_issues']), 1) self.assertEqual(len(response.json()[0]['related_issues']), 1)
self.assertEqual(response.json()[0]['related_issues'][0]['name'], "test issue") self.assertEqual(response.json()[0]['related_issues'][0]['name'], "test issue")
self.assertEqual(response.json()[0]['related_issues'][0]['event'], "EVENT") self.assertEqual(response.json()[0]['related_issues'][0]['event'], "EVENT")
@ -93,15 +78,15 @@ class ItemTestCase(TestCase):
def test_members_with_file(self): def test_members_with_file(self):
import base64 import base64
item = Item.objects.create(container=self.box1, event=self.event, description='1') item = Item.objects.create(container=self.box, event=self.event, description='1')
file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')) file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
response = self.client.get(f'/api/2/{self.event.slug}/item/') response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1) self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['id'], item.id) self.assertEqual(response.json()[0]['id'], item.id)
self.assertEqual(response.json()[0]['description'], '1') self.assertEqual(response.json()[0]['description'], '1')
self.assertEqual(response.json()[0]['box'], 'BOX1') self.assertEqual(response.json()[0]['box'], 'BOX')
self.assertEqual(response.json()[0]['cid'], self.box1.id) self.assertEqual(response.json()[0]['cid'], self.box.id)
self.assertEqual(response.json()[0]['file'], file.hash) self.assertEqual(response.json()[0]['file'], file.hash)
self.assertEqual(response.json()[0]['returned'], False) self.assertEqual(response.json()[0]['returned'], False)
self.assertEqual(response.json()[0]['event'], self.event.slug) self.assertEqual(response.json()[0]['event'], self.event.slug)
@ -109,38 +94,36 @@ class ItemTestCase(TestCase):
def test_members_with_two_file(self): def test_members_with_two_file(self):
import base64 import base64
item = Item.objects.create(container=self.box1, event=self.event, description='1') item = Item.objects.create(container=self.box, event=self.event, description='1')
file1 = File.objects.create(item=item, file1 = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')) file2 = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"bar").decode('utf-8'))
file2 = File.objects.create(item=item,
data="data:text/plain;base64," + base64.b64encode(b"bar").decode('utf-8'))
response = self.client.get(f'/api/2/{self.event.slug}/item/') response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1) self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['id'], item.id) self.assertEqual(response.json()[0]['id'], item.id)
self.assertEqual(response.json()[0]['description'], '1') self.assertEqual(response.json()[0]['description'], '1')
self.assertEqual(response.json()[0]['box'], 'BOX1') self.assertEqual(response.json()[0]['box'], 'BOX')
self.assertEqual(response.json()[0]['cid'], self.box1.id) self.assertEqual(response.json()[0]['cid'], self.box.id)
self.assertEqual(response.json()[0]['file'], file2.hash) self.assertEqual(response.json()[0]['file'], file2.hash)
self.assertEqual(response.json()[0]['returned'], False) self.assertEqual(response.json()[0]['returned'], False)
self.assertEqual(response.json()[0]['event'], self.event.slug) self.assertEqual(response.json()[0]['event'], self.event.slug)
self.assertEqual(len(response.json()[0]['related_issues']), 0) self.assertEqual(len(response.json()[0]['related_issues']), 0)
def test_multi_members(self): def test_multi_members(self):
Item.objects.create(container=self.box1, event=self.event, description='1') Item.objects.create(container=self.box, event=self.event, description='1')
Item.objects.create(container=self.box1, event=self.event, description='2') Item.objects.create(container=self.box, event=self.event, description='2')
Item.objects.create(container=self.box1, event=self.event, description='3') Item.objects.create(container=self.box, event=self.event, description='3')
response = self.client.get(f'/api/2/{self.event.slug}/item/') response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 3) self.assertEqual(len(response.json()), 3)
def test_create_item(self): def test_create_item(self):
response = self.client.post(f'/api/2/{self.event.slug}/item/', {'cid': self.box1.id, 'description': '1'}) response = self.client.post(f'/api/2/{self.event.slug}/item/', {'cid': self.box.id, 'description': '1'})
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['id'], 1) self.assertEqual(response.json()['id'], 1)
self.assertEqual(response.json()['description'], '1') self.assertEqual(response.json()['description'], '1')
self.assertEqual(response.json()['box'], 'BOX1') self.assertEqual(response.json()['box'], 'BOX')
self.assertEqual(response.json()['cid'], self.box1.id) self.assertEqual(response.json()['cid'], self.box.id)
self.assertEqual(response.json()['file'], None) self.assertEqual(response.json()['file'], None)
self.assertEqual(response.json()['returned'], False) self.assertEqual(response.json()['returned'], False)
self.assertEqual(response.json()['event'], self.event.slug) self.assertEqual(response.json()['event'], self.event.slug)
@ -148,43 +131,43 @@ class ItemTestCase(TestCase):
self.assertEqual(len(Item.objects.all()), 1) self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].id, 1) self.assertEqual(Item.objects.all()[0].id, 1)
self.assertEqual(Item.objects.all()[0].description, '1') self.assertEqual(Item.objects.all()[0].description, '1')
self.assertEqual(Item.objects.all()[0].container.id, self.box1.id) self.assertEqual(Item.objects.all()[0].container.id, self.box.id)
def test_create_item_without_container(self): def test_create_item_without_container(self):
response = self.client.post(f'/api/2/{self.event.slug}/item/', {'description': '1'}) response = self.client.post(f'/api/2/{self.event.slug}/item/', {'description': '1'})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_create_item_without_description(self): def test_create_item_without_description(self):
response = self.client.post(f'/api/2/{self.event.slug}/item/', {'cid': self.box1.id}) response = self.client.post(f'/api/2/{self.event.slug}/item/', {'cid': self.box.id})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_create_item_with_file(self): def test_create_item_with_file(self):
import base64 import base64
response = self.client.post(f'/api/2/{self.event.slug}/item/', response = self.client.post(f'/api/2/{self.event.slug}/item/',
{'cid': self.box1.id, 'description': '1', {'cid': self.box.id, 'description': '1',
'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode( 'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode(
'utf-8')}, content_type='application/json') 'utf-8')}, content_type='application/json')
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['id'], 1) self.assertEqual(response.json()['id'], 1)
self.assertEqual(response.json()['description'], '1') self.assertEqual(response.json()['description'], '1')
self.assertEqual(response.json()['box'], 'BOX1') self.assertEqual(response.json()['box'], 'BOX')
self.assertEqual(response.json()['id'], self.box1.id) self.assertEqual(response.json()['id'], self.box.id)
self.assertEqual(len(response.json()['file']), 64) self.assertEqual(len(response.json()['file']), 64)
self.assertEqual(len(Item.objects.all()), 1) self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].id, 1) self.assertEqual(Item.objects.all()[0].id, 1)
self.assertEqual(Item.objects.all()[0].description, '1') self.assertEqual(Item.objects.all()[0].description, '1')
self.assertEqual(Item.objects.all()[0].container.id, self.box1.id) self.assertEqual(Item.objects.all()[0].container.id, self.box.id)
self.assertEqual(len(File.objects.all()), 1) self.assertEqual(len(File.objects.all()), 1)
def test_update_item(self): def test_update_item(self):
item = Item.objects.create(container=self.box1, event=self.event, description='1') item = Item.objects.create(container=self.box, event=self.event, description='1')
response = self.client.patch(f'/api/2/{self.event.slug}/item/{item.id}/', {'description': '2'}, response = self.client.patch(f'/api/2/{self.event.slug}/item/{item.id}/', {'description': '2'},
content_type='application/json') content_type='application/json')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['id'], item.id) self.assertEqual(response.json()['id'], item.id)
self.assertEqual(response.json()['description'], '2') self.assertEqual(response.json()['description'], '2')
self.assertEqual(response.json()['box'], 'BOX1') self.assertEqual(response.json()['box'], 'BOX')
self.assertEqual(response.json()['cid'], self.box1.id) self.assertEqual(response.json()['cid'], self.box.id)
self.assertEqual(response.json()['file'], None) self.assertEqual(response.json()['file'], None)
self.assertEqual(response.json()['returned'], False) self.assertEqual(response.json()['returned'], False)
self.assertEqual(response.json()['event'], self.event.slug) self.assertEqual(response.json()['event'], self.event.slug)
@ -192,62 +175,60 @@ class ItemTestCase(TestCase):
self.assertEqual(len(Item.objects.all()), 1) self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].id, 1) self.assertEqual(Item.objects.all()[0].id, 1)
self.assertEqual(Item.objects.all()[0].description, '2') self.assertEqual(Item.objects.all()[0].description, '2')
self.assertEqual(Item.objects.all()[0].container.id, self.box1.id) self.assertEqual(Item.objects.all()[0].container.id, self.box.id)
def test_update_item_with_file(self): def test_update_item_with_file(self):
import base64 import base64
item = Item.objects.create(container=self.box1, event=self.event, description='1') item = Item.objects.create(container=self.box, event=self.event, description='1')
response = self.client.patch(f'/api/2/{self.event.slug}/item/{item.id}/', response = self.client.patch(f'/api/2/{self.event.slug}/item/{item.id}/',
{'description': '2', {'description': '2',
'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode( 'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')},
'utf-8')}, content_type='application/json')
content_type='application/json')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['id'], 1) self.assertEqual(response.json()['id'], 1)
self.assertEqual(response.json()['description'], '2') self.assertEqual(response.json()['description'], '2')
self.assertEqual(response.json()['box'], 'BOX1') self.assertEqual(response.json()['box'], 'BOX')
self.assertEqual(response.json()['id'], self.box1.id) self.assertEqual(response.json()['id'], self.box.id)
self.assertEqual(len(response.json()['file']), 64) self.assertEqual(len(response.json()['file']), 64)
self.assertEqual(len(Item.objects.all()), 1) self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].id, 1) self.assertEqual(Item.objects.all()[0].id, 1)
self.assertEqual(Item.objects.all()[0].description, '2') self.assertEqual(Item.objects.all()[0].description, '2')
self.assertEqual(Item.objects.all()[0].container.id, self.box1.id) self.assertEqual(Item.objects.all()[0].container.id, self.box.id)
self.assertEqual(len(File.objects.all()), 1) self.assertEqual(len(File.objects.all()), 1)
def test_delete_item(self): def test_delete_item(self):
item = Item.objects.create(container=self.box1, event=self.event, description='1') item = Item.objects.create(container=self.box, event=self.event, description='1')
Item.objects.create(container=self.box1, event=self.event, description='2') Item.objects.create(container=self.box, event=self.event, description='2')
self.assertEqual(len(Item.objects.all()), 2) self.assertEqual(len(Item.objects.all()), 2)
response = self.client.delete(f'/api/2/{self.event.slug}/item/{item.id}/') response = self.client.delete(f'/api/2/{self.event.slug}/item/{item.id}/')
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)
self.assertEqual(len(Item.objects.all()), 1) self.assertEqual(len(Item.objects.all()), 1)
def test_delete_item2(self): def test_delete_item2(self):
Item.objects.create(container=self.box1, event=self.event, description='1') Item.objects.create(container=self.box, event=self.event, description='1')
item2 = Item.objects.create(container=self.box1, event=self.event, description='2') item2 = Item.objects.create(container=self.box, event=self.event, description='2')
self.assertEqual(len(Item.objects.all()), 2) self.assertEqual(len(Item.objects.all()), 2)
response = self.client.delete(f'/api/2/{self.event.slug}/item/{item2.id}/') response = self.client.delete(f'/api/2/{self.event.slug}/item/{item2.id}/')
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)
self.assertEqual(len(Item.objects.all()), 1) self.assertEqual(len(Item.objects.all()), 1)
item3 = Item.objects.create(container=self.box1, event=self.event, description='3') item3 = Item.objects.create(container=self.box, event=self.event, description='3')
self.assertEqual(item3.id, 3) self.assertEqual(item3.id, 3)
self.assertEqual(len(Item.objects.all()), 2) self.assertEqual(len(Item.objects.all()), 2)
def test_item_count(self): def test_item_count(self):
Item.objects.create(container=self.box1, event=self.event, description='1') Item.objects.create(container=self.box, event=self.event, description='1')
Item.objects.create(container=self.box1, event=self.event, description='2') Item.objects.create(container=self.box, event=self.event, description='2')
response = self.client.get('/api/2/boxes/') response = self.client.get('/api/2/boxes/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 2) self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['itemCount'], 2) self.assertEqual(response.json()[0]['itemCount'], 2)
self.assertEqual(response.json()[1]['itemCount'], 0)
def test_item_nonexistent(self): def test_item_nonexistent(self):
response = self.client.get(f'/api/2/NOEVENT/item/') response = self.client.get(f'/api/2/NOEVENT/item/')
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_item_return(self): def test_item_return(self):
item = Item.objects.create(container=self.box1, event=self.event, description='1') item = Item.objects.create(container=self.box, event=self.event, description='1')
self.assertEqual(item.returned_at, None) self.assertEqual(item.returned_at, None)
response = self.client.get(f'/api/2/{self.event.slug}/item/') response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -262,8 +243,8 @@ class ItemTestCase(TestCase):
self.assertEqual(len(response.json()), 0) self.assertEqual(len(response.json()), 0)
def test_item_show_not_returned(self): def test_item_show_not_returned(self):
item1 = Item.objects.create(container=self.box1, event=self.event, description='1') item1 = Item.objects.create(container=self.box, event=self.event, description='1')
item2 = Item.objects.create(container=self.box1, event=self.event, description='2') item2 = Item.objects.create(container=self.box, event=self.event, description='2')
response = self.client.get(f'/api/2/{self.event.slug}/item/') response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 2) self.assertEqual(len(response.json()), 2)
@ -280,15 +261,15 @@ class ItemSearchTestCase(TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.event = Event.objects.create(slug='EVENT', name='Event') self.event = Event.objects.create(slug='EVENT', name='Event')
self.box1 = Container.objects.create(name='BOX1') self.box = Container.objects.create(name='BOX')
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.user_permissions.add(*Permission.objects.all()) self.user.user_permissions.add(*Permission.objects.all())
self.token = AuthToken.objects.create(user=self.user) self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
self.item1 = Item.objects.create(container=self.box1, event=self.event, description='abc def') self.item1 = Item.objects.create(container=self.box, event=self.event, description='abc def')
self.item2 = Item.objects.create(container=self.box1, event=self.event, description='def ghi') self.item2 = Item.objects.create(container=self.box, event=self.event, description='def ghi')
self.item3 = Item.objects.create(container=self.box1, event=self.event, description='jkl mno pqr') self.item3 = Item.objects.create(container=self.box, event=self.event, description='jkl mno pqr')
self.item4 = Item.objects.create(container=self.box1, event=self.event, description='stu vwx') self.item4 = Item.objects.create(container=self.box, event=self.event, description='stu vwx')
def test_search(self): def test_search(self):
search_query = b64encode(b'abc').decode('utf-8') search_query = b64encode(b'abc').decode('utf-8')
@ -297,8 +278,8 @@ class ItemSearchTestCase(TestCase):
self.assertEqual(1, len(response.json())) self.assertEqual(1, len(response.json()))
self.assertEqual(self.item1.id, response.json()[0]['id']) self.assertEqual(self.item1.id, response.json()[0]['id'])
self.assertEqual('abc def', response.json()[0]['description']) self.assertEqual('abc def', response.json()[0]['description'])
self.assertEqual('BOX1', response.json()[0]['box']) self.assertEqual('BOX', response.json()[0]['box'])
self.assertEqual(self.box1.id, response.json()[0]['cid']) self.assertEqual(self.box.id, response.json()[0]['cid'])
self.assertEqual(1, response.json()[0]['search_score']) self.assertEqual(1, response.json()[0]['search_score'])
def test_search2(self): def test_search2(self):
@ -308,13 +289,13 @@ class ItemSearchTestCase(TestCase):
self.assertEqual(2, len(response.json())) self.assertEqual(2, len(response.json()))
self.assertEqual(self.item1.id, response.json()[0]['id']) self.assertEqual(self.item1.id, response.json()[0]['id'])
self.assertEqual('abc def', response.json()[0]['description']) self.assertEqual('abc def', response.json()[0]['description'])
self.assertEqual('BOX1', response.json()[0]['box']) self.assertEqual('BOX', response.json()[0]['box'])
self.assertEqual(self.box1.id, response.json()[0]['cid']) self.assertEqual(self.box.id, response.json()[0]['cid'])
self.assertEqual(1, response.json()[0]['search_score']) self.assertEqual(1, response.json()[0]['search_score'])
self.assertEqual(self.item2.id, response.json()[1]['id']) self.assertEqual(self.item2.id, response.json()[1]['id'])
self.assertEqual('def ghi', response.json()[1]['description']) self.assertEqual('def ghi', response.json()[1]['description'])
self.assertEqual('BOX1', response.json()[1]['box']) self.assertEqual('BOX', response.json()[1]['box'])
self.assertEqual(self.box1.id, response.json()[1]['cid']) self.assertEqual(self.box.id, response.json()[1]['cid'])
self.assertEqual(1, response.json()[0]['search_score']) self.assertEqual(1, response.json()[0]['search_score'])
def test_search3(self): def test_search3(self):
@ -324,8 +305,8 @@ class ItemSearchTestCase(TestCase):
self.assertEqual(1, len(response.json())) self.assertEqual(1, len(response.json()))
self.assertEqual(self.item3.id, response.json()[0]['id']) self.assertEqual(self.item3.id, response.json()[0]['id'])
self.assertEqual('jkl mno pqr', response.json()[0]['description']) self.assertEqual('jkl mno pqr', response.json()[0]['description'])
self.assertEqual('BOX1', response.json()[0]['box']) self.assertEqual('BOX', response.json()[0]['box'])
self.assertEqual(self.box1.id, response.json()[0]['cid']) self.assertEqual(self.box.id, response.json()[0]['cid'])
self.assertEqual(1, response.json()[0]['search_score']) self.assertEqual(1, response.json()[0]['search_score'])
def test_search4(self): def test_search4(self):
@ -335,11 +316,12 @@ class ItemSearchTestCase(TestCase):
self.assertEqual(2, len(response.json())) self.assertEqual(2, len(response.json()))
self.assertEqual(self.item1.id, response.json()[0]['id']) self.assertEqual(self.item1.id, response.json()[0]['id'])
self.assertEqual('abc def', response.json()[0]['description']) self.assertEqual('abc def', response.json()[0]['description'])
self.assertEqual('BOX1', response.json()[0]['box']) self.assertEqual('BOX', response.json()[0]['box'])
self.assertEqual(self.box1.id, response.json()[0]['cid']) self.assertEqual(self.box.id, response.json()[0]['cid'])
self.assertEqual(2, response.json()[0]['search_score']) self.assertEqual(2, response.json()[0]['search_score'])
self.assertEqual(self.item2.id, response.json()[1]['id']) self.assertEqual(self.item2.id, response.json()[1]['id'])
self.assertEqual('def ghi', response.json()[1]['description']) self.assertEqual('def ghi', response.json()[1]['description'])
self.assertEqual('BOX1', response.json()[1]['box']) self.assertEqual('BOX', response.json()[1]['box'])
self.assertEqual(self.box1.id, response.json()[1]['cid']) self.assertEqual(self.box.id, response.json()[1]['cid'])
self.assertEqual(1, response.json()[1]['search_score']) self.assertEqual(1, response.json()[1]['search_score'])

View file

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

View file

@ -42,27 +42,19 @@ export default {
url: this.src, url: this.src,
data: this.image_data data: this.image_data
}); });
},
deferImage() {
setTimeout(() => {
if (this.cached) {
const c = this.getThumbnail(this.src);
if (c) {
this.image_data = c;
return;
}
}
this.loadImage();
}, 0);
}
},
watch: {
src: function (newVal, oldVal) {
this.deferImage()
} }
}, },
mounted() { mounted() {
this.deferImage(); setTimeout(() => {
if (this.cached) {
const c = this.getThumbnail(this.src);
if (c) {
this.image_data = c;
return;
}
}
this.loadImage();
}, 0);
} }
} }
</script> </script>

View file

@ -0,0 +1,222 @@
<template>
<ol class="timeline">
<li v-for="(item, index) in timeline" :key="index"
:class="{'timeline-item':true, 'extra-space': item.type === 'mail'}">
<span class="timeline-item-icon filled-icon" v-if="item.type === 'mail'">
<font-awesome-icon icon="envelope"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'comment'">
<font-awesome-icon icon="comment"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'state'"
:class="'bg-' + stateInfo(item.state).color">
<font-awesome-icon :icon="stateInfo(item.state).icon"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'assignment'" :class="'bg-secondary'">
<font-awesome-icon icon="user"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'item_relation'">
<font-awesome-icon icon="object-group"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'shipping_voucher'">
<font-awesome-icon icon="truck"/>
</span>
<span class="timeline-item-icon faded-icon" v-else>
<font-awesome-icon icon="pen"/>
</span>
<TimelineMail v-if="item.type === 'mail'" :item="item"/>
<TimelineComment v-else-if="item.type === 'comment'" :item="item"/>
<TimelineStateChange v-else-if="item.type === 'state'" :item="item"/>
<TimelineAssignment v-else-if="item.type === 'assignment'" :item="item"/>
<TimelineRelatedItem v-else-if="item.type === 'item_relation'" :item="item"/>
<TimelineShippingVoucher v-else-if="item.type === 'shipping_voucher'" :item="item"/>
<p v-else>{{ item }}</p>
</li>
<li class="timeline-item">
<span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="comment"/>
</span>
<div class="new-comment card bg-dark">
<div class="">
<textarea placeholder="add comment..." v-model="newComment" class="form-control">
</textarea>
<AsyncButton class="btn btn-primary float-right" :task="addCommentAndClear">
<font-awesome-icon icon="comment"/>
Save Comment
</AsyncButton>
</div>
</div>
</li>
</ol>
</template>
<script>
import TimelineMail from "@/components/TimelineMail.vue";
import TimelineComment from "@/components/TimelineComment.vue";
import TimelineStateChange from "@/components/TimelineStateChange.vue";
import {mapActions, mapGetters} from "vuex";
import TimelineAssignment from "@/components/TimelineAssignment.vue";
import TimelineRelatedItem from "@/components/TimelineRelatedItem.vue";
import TimelineShippingVoucher from "@/components/TimelineShippingVoucher.vue";
import AsyncButton from "@/components/inputs/AsyncButton.vue";
export default {
name: 'Timeline',
components: {
TimelineShippingVoucher, AsyncButton,
TimelineRelatedItem, TimelineAssignment, TimelineStateChange, TimelineComment, TimelineMail
},
props: {
timeline: {
type: Array,
default: () => []
}
},
emits: ['sendMail', 'addComment'],
data: () => ({
newMail: "",
newComment: ""
}),
computed: {
...mapGetters(['stateInfo']),
newestMailSubject() {
const mail = this.timeline.filter(item => item.type === 'mail').pop();
return mail ? mail.subject : "";
},
},
methods: {
...mapActions(['sendMail', 'postComment']),
sendMailAndClear: async function () {
await this.sendMail(this.newMail);
this.newMail = "";
},
addCommentAndClear: async function () {
await this.postComment(this.newComment);
this.newComment = "";
}
}
};
</script>
<style lang="scss" scoped>
*,
*:before,
*:after {
box-sizing: border-box;
}
button,
input,
select,
textarea {
font: inherit;
}
a {
color: inherit;
}
img {
display: block;
max-width: 100%;
}
/* End basic CSS override */
.timeline {
width: 85%;
display: flex;
flex-direction: column;
padding: 32px 0 32px 32px;
border-left: 2px solid var(--gray);
font-size: 1.125rem;
margin: 0 auto;
}
.timeline-item {
display: flex;
gap: 24px;
& + * {
margin-top: 24px;
}
& + .extra-space {
margin-top: 48px;
}
}
.new-comment, .new-mail {
width: 100%;
textarea, input {
border: 1px solid var(--gray);
border-radius: 6px;
height: 5em;
padding: 8px 16px;
&::placeholder {
color: var(--gray-dark);
}
&:focus {
border-color: var(--gray-dark);
outline: 0; /* Don't actually do this */
box-shadow: 0 0 0 4px var(--dark);
}
}
}
.timeline-item-icon {
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 50%;
margin-left: -57px;
flex-shrink: 0;
overflow: hidden;
svg {
width: 20px;
height: 20px;
}
&.faded-icon {
background-color: var(--secondary);
color: var(--light);
}
&.filled-icon {
background-color: var(--primary);
color: var(--light);
}
}
.button {
border: 0;
display: inline-flex;
vertical-align: middle;
margin-right: 4px;
margin-top: 12px;
align-items: center;
justify-content: center;
font-size: 1rem;
height: 32px;
padding: 0 8px;
background-color: var(--dark);
flex-shrink: 0;
cursor: pointer;
border-radius: 99em;
&:hover {
background-color: var(--gray);
}
}
</style>

View file

@ -21,9 +21,6 @@
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'shipping_voucher'"> <span class="timeline-item-icon faded-icon" v-else-if="item.type === 'shipping_voucher'">
<font-awesome-icon icon="truck"/> <font-awesome-icon icon="truck"/>
</span> </span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'placement'">
<font-awesome-icon icon="archive"/>
</span>
<span class="timeline-item-icon faded-icon" v-else> <span class="timeline-item-icon faded-icon" v-else>
<font-awesome-icon icon="pen"/> <font-awesome-icon icon="pen"/>
</span> </span>
@ -33,15 +30,40 @@
<TimelineAssignment v-else-if="item.type === 'assignment'" :item="item"/> <TimelineAssignment v-else-if="item.type === 'assignment'" :item="item"/>
<TimelineRelatedItem v-else-if="item.type === 'item_relation'" :item="item"/> <TimelineRelatedItem v-else-if="item.type === 'item_relation'" :item="item"/>
<TimelineShippingVoucher v-else-if="item.type === 'shipping_voucher'" :item="item"/> <TimelineShippingVoucher v-else-if="item.type === 'shipping_voucher'" :item="item"/>
<TimelinePlacement v-else-if="item.type === 'placement'" :item="item"/>
<TimelineRelatedTicket v-else-if="item.type === 'issue_relation'" :item="item"/>
<p v-else>{{ item }}</p> <p v-else>{{ item }}</p>
</li> </li>
<li class="timeline-item"> <li class="timeline-item">
<slot name="timeline_action1"/> <span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="comment"/>
</span>
<div class="new-comment card bg-dark">
<div class="">
<textarea placeholder="add comment..." v-model="newComment" class="form-control">
</textarea>
<AsyncButton class="btn btn-primary float-right" :task="addCommentAndClear">
<font-awesome-icon icon="comment"/>
Save Comment
</AsyncButton>
</div>
</div>
</li> </li>
<li class="timeline-item"> <li class="timeline-item">
<slot name="timeline_action2"/> <span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="envelope"/>
</span>
<div class="new-mail card bg-dark">
<div class="card-header">
{{ newestMailSubject }}
</div>
<div>
<textarea placeholder="reply mail..." v-model="newMail" class="form-control">
</textarea>
<AsyncButton class="btn btn-primary float-right" :task="sendMailAndClear">
<font-awesome-icon icon="envelope"/>
Send Mail
</AsyncButton>
</div>
</div>
</li> </li>
</ol> </ol>
</template> </template>
@ -56,20 +78,12 @@ import TimelineAssignment from "@/components/TimelineAssignment.vue";
import TimelineRelatedItem from "@/components/TimelineRelatedItem.vue"; import TimelineRelatedItem from "@/components/TimelineRelatedItem.vue";
import TimelineShippingVoucher from "@/components/TimelineShippingVoucher.vue"; import TimelineShippingVoucher from "@/components/TimelineShippingVoucher.vue";
import AsyncButton from "@/components/inputs/AsyncButton.vue"; import AsyncButton from "@/components/inputs/AsyncButton.vue";
import TimelinePlacement from "@/components/TimelinePlacement.vue";
import TimelineRelatedTicket from "@/components/TimelineRelatedTicket.vue";
export default { export default {
name: 'Timeline', name: 'Timeline',
components: { components: {
TimelineRelatedTicket, TimelineShippingVoucher, AsyncButton,
TimelinePlacement, TimelineRelatedItem, TimelineAssignment, TimelineStateChange, TimelineComment, TimelineMail
TimelineShippingVoucher,
TimelineRelatedItem,
TimelineAssignment,
TimelineStateChange,
TimelineComment,
TimelineMail
}, },
props: { props: {
timeline: { timeline: {
@ -77,13 +91,33 @@ export default {
default: () => [] default: () => []
} }
}, },
emits: ['sendMail', 'addComment'],
data: () => ({
newMail: "",
newComment: ""
}),
computed: { computed: {
...mapGetters(['stateInfo']) ...mapGetters(['stateInfo']),
newestMailSubject() {
const mail = this.timeline.filter(item => item.type === 'mail').pop();
return mail ? mail.subject : "";
},
}, },
methods: {
...mapActions(['sendMail', 'postComment']),
sendMailAndClear: async function () {
await this.sendMail(this.newMail);
this.newMail = "";
},
addCommentAndClear: async function () {
await this.postComment(this.newComment);
this.newComment = "";
}
}
}; };
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
*, *,
*:before, *:before,
@ -102,10 +136,10 @@ a {
color: inherit; color: inherit;
} }
/*img { img {
display: block; display: block;
max-width: 100%; max-width: 100%;
}*/ }
/* End basic CSS override */ /* End basic CSS override */

View file

@ -1,85 +0,0 @@
<template>
<div class="timeline-item-description">
<i class="avatar | small">
<font-awesome-icon icon="user"/>
</i>
<span><a href="#">$USER</a> has placed item in '{{item.box}}' (#{{item.cid}}) at <time
:datetime="timestamp">{{ timestamp }}</time>
</span>
</div>
</template>
<script>
export default {
name: 'TimelinePlacement',
props: {
'item': {
type: Object,
required: true
}
},
computed: {
'timestamp': function () {
return new Date(this.item.timestamp).toLocaleString();
},
}
};
</script>
<style scoped>
a {
color: inherit;
}
/* End basic CSS override */
.timeline-item-description {
display: flex;
padding-top: 6px;
gap: 8px;
color: var(--gray);
img {
flex-shrink: 0;
}
a {
/*color: var(--c-grey-500);*/
font-weight: 500;
text-decoration: none;
&:hover,
&:focus {
outline: 0; /* Don't actually do this */
color: var(--info);
}
}
}
.avatar {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
overflow: hidden;
aspect-ratio: 1 / 1;
flex-shrink: 0;
width: 40px;
height: 40px;
&.small {
width: 28px;
height: 28px;
}
img {
object-fit: cover;
}
}
</style>

View file

@ -5,12 +5,8 @@
<i class="avatar | small"> <i class="avatar | small">
<font-awesome-icon icon="user"/> <font-awesome-icon icon="user"/>
</i> </i>
<span><!--a href="#">$USER</a--> linked item <span class="badge badge-secondary">#{{ <span><!--a href="#">$USER</a--> linked item <span class="badge badge-secondary">#{{ item.item.uid }} </span> on <time
item.item.id :datetime="timestamp">{{ timestamp }}</time> as <span class="badge badge-primary">{{ item.status }}</span>
}} </span> on <time
:datetime="timestamp">{{ timestamp }}</time> as <span class="badge badge-primary">{{
item.status
}}</span>
</span> </span>
</div> </div>
<div class="card bg-dark"> <div class="card bg-dark">
@ -24,10 +20,8 @@
</div> </div>
<div class="col"> <div class="col">
<div class="card-body"> <div class="card-body">
<h6 class="card-subtitle text-secondary">id: {{ item.item.id }} box: {{ item.item.box }}</h6> <h6 class="card-subtitle text-secondary">uid: {{ item.item.uid }} box: {{ item.item.box }}</h6>
<router-link :to="{name: 'item', params: {id: item.item.id}}"> <h6 class="card-title">{{ item.item.description }}</h6>
<h6 class="card-title">{{ item.item.description }}</h6>
</router-link>
<!--div class="row mx-auto mt-2"> <!--div class="row mx-auto mt-2">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-outline-success" <button class="btn btn-outline-success"

View file

@ -1,98 +0,0 @@
<template>
<div class="timeline-item-wrapper">
<div class="timeline-item-description">
<i class="avatar | small">
<font-awesome-icon icon="user"/>
</i>
<span> linked ticket <span class="badge badge-secondary">#{{ item.issue_thread.id }} </span> on
<time :datetime="timestamp">{{ timestamp }}</time> as
<span class="badge badge-primary">{{ item.status }}</span>
</span>
</div>
<div class="timeline-item-description">
<router-link :to="{name: 'ticket', params: {id: item.issue_thread.id}}">
<h6 class="card-title">Ticket #{{ item.issue_thread.id }} - {{ item.issue_thread.name }}</h6>
</router-link>
</div>
</div>
</template>
<script>
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue";
import Lightbox from "@/components/Lightbox.vue";
export default {
name: 'TimelineRelatedTicket',
components: {Lightbox},
props: {
'item': {
type: Object,
required: true
}
},
computed: {
'timestamp': function () {
return new Date(this.item.timestamp).toLocaleString();
}
},
};
</script>
<style scoped>
a {
color: inherit;
}
.timeline-item-description {
display: flex;
padding-top: 6px;
gap: 8px;
color: var(--gray);
img {
flex-shrink: 0;
}
a {
/*color: var(--c-grey-500);*/
font-weight: 500;
text-decoration: none;
&:hover,
&:focus {
outline: 0; /* Don't actually do this */
color: var(--info);
}
}
}
.card {
border: 1px solid var(--gray);
}
.avatar {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
overflow: hidden;
aspect-ratio: 1 / 1;
flex-shrink: 0;
width: 40px;
height: 40px;
&.small {
width: 28px;
height: 28px;
}
img {
object-fit: cover;
}
}
</style>

View file

@ -43,11 +43,11 @@ export default {
props: ['label', 'model', 'nameKey', 'uniqueKey', 'options', 'onOptionAdd'], props: ['label', 'model', 'nameKey', 'uniqueKey', 'options', 'onOptionAdd'],
data: ({options, model, nameKey, uniqueKey}) => ({ data: ({options, model, nameKey, uniqueKey}) => ({
internalName: model[nameKey], internalName: model[nameKey],
selectedOption: options.filter(e => e[uniqueKey] === model[uniqueKey])[0], selectedOption: options.filter(e => e[uniqueKey] == model[uniqueKey])[0],
addingOption: false addingOption: false
}), }),
computed: { computed: {
isValid: ({options, nameKey, internalName}) => options.some(e => e[nameKey] === internalName), isValid: ({options, nameKey, internalName}) => options.some(e => e[nameKey] == internalName),
sortedOptions: ({ sortedOptions: ({
options, options,
nameKey nameKey
@ -56,7 +56,7 @@ export default {
watch: { watch: {
internalName(newValue) { internalName(newValue) {
if (this.isValid) { if (this.isValid) {
if (!this.selectedOption || newValue !== this.selectedOption[this.nameKey]) { if (!this.selectedOption || newValue != this.selectedOption[this.nameKey]) {
this.selectedOption = this.options.filter(e => e[this.nameKey] === newValue)[0]; this.selectedOption = this.options.filter(e => e[this.nameKey] === newValue)[0];
} }
this.model[this.nameKey] = this.selectedOption[this.nameKey]; this.model[this.nameKey] = this.selectedOption[this.nameKey];

View file

@ -1,6 +1,6 @@
import {createApp} from 'vue' import {createApp} from 'vue'
import App from './App.vue'; import App from './App.vue';
//import VueQrcode from '@chenfengyuan/vue-qrcode'; import VueQrcode from '@chenfengyuan/vue-qrcode';
import store from './store'; import store from './store';
import router from './router'; import router from './router';
@ -53,6 +53,6 @@ library.add(faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, fa
const app = createApp(App).use(store).use(router); const app = createApp(App).use(store).use(router);
//app.component(VueQrcode.name, VueQrcode); app.component(VueQrcode.name, VueQrcode);
app.component('font-awesome-icon', FontAwesomeIcon); app.component('font-awesome-icon', FontAwesomeIcon);
app.mount('#app') app.mount('#app')

View file

@ -37,7 +37,6 @@ const store = createStore({
expiry: null, expiry: null,
}, },
lightboxHash: null,
thumbnailCache: {}, thumbnailCache: {},
fetchedData: { fetchedData: {
events: 0, events: 0,
@ -160,7 +159,7 @@ const store = createStore({
}, },
updateItem(state, updatedItem) { updateItem(state, updatedItem) {
const item = state.loadedItems[updatedItem.event ? updatedItem.event : 'none'].filter( const item = state.loadedItems[updatedItem.event ? updatedItem.event : 'none'].filter(
({id}) => id === updatedItem.id)[0]; ({uid}) => uid === updatedItem.uid)[0];
Object.assign(item, updatedItem); Object.assign(item, updatedItem);
}, },
removeItem(state, item) { removeItem(state, item) {
@ -192,9 +191,6 @@ const store = createStore({
state.groups = groups; state.groups = groups;
state.fetchedData = {...state.fetchedData, groups: Date.now()}; state.fetchedData = {...state.fetchedData, groups: Date.now()};
}, },
openLightboxModalWith(state, hash) {
state.lightboxHash = hash;
},
openAddBoxModal(state) { openAddBoxModal(state) {
state.showAddBoxModal = true; state.showAddBoxModal = true;
}, },
@ -356,13 +352,14 @@ const store = createStore({
const {data, success} = await getters.session.delete(`/2/events/${event_id}/`); const {data, success} = await getters.session.delete(`/2/events/${event_id}/`);
if (success) { if (success) {
await dispatch('loadEvents') await dispatch('loadEvents')
commit('replaceEvents', [...state.events.filter(e => e.id !== event_id)]) commit('replaceEvents', [...state.events.filter(e => e.eid !== event_id)])
} }
}, },
async updateEvent({commit, dispatch, state}, {id, partial_event}) { async updateEvent({commit, dispatch, state}, {id, partial_event}){
console.log(id, partial_event);
const {data, success} = await http.patch(`/2/events/${id}/`, partial_event, state.user.token); const {data, success} = await http.patch(`/2/events/${id}/`, partial_event, state.user.token);
if (success) { if (success) {
commit('replaceEvents', [...state.events.filter(e => e.id !== id), data]) commit('replaceEvents', [...state.events.filter(e => e.eid !== id), data])
} }
}, },
async fetchTicketStates({commit, state, getters}) { async fetchTicketStates({commit, state, getters}) {
@ -373,6 +370,7 @@ const store = createStore({
}, },
async changeEvent({dispatch, getters, commit}, eventName) { async changeEvent({dispatch, getters, commit}, eventName) {
await router.push({path: `/${eventName.slug}/${getters.getActiveView}/`}); await router.push({path: `/${eventName.slug}/${getters.getActiveView}/`});
//dispatch('loadEventItems');
}, },
async changeView({getters}, link) { async changeView({getters}, link) {
await router.push({path: `/${getters.getEventSlug}/${link.path}/`}); await router.push({path: `/${getters.getEventSlug}/${link.path}/`});
@ -423,16 +421,16 @@ const store = createStore({
async updateItem({commit, getters, state}, item) { async updateItem({commit, getters, state}, item) {
const { const {
data, success data, success
} = await getters.session.put(`/2/${getters.getEventSlug}/item/${item.id}/`, item); } = await getters.session.put(`/2/${getters.getEventSlug}/item/${item.uid}/`, item);
commit('updateItem', data); commit('updateItem', data);
}, },
async markItemReturned({commit, getters, state}, item) { async markItemReturned({commit, getters, state}, item) {
await getters.session.patch(`/2/${getters.getEventSlug}/item/${item.id}/`, {returned: true}, await getters.session.patch(`/2/${getters.getEventSlug}/item/${item.uid}/`, {returned: true},
state.user.token); state.user.token);
commit('removeItem', item); commit('removeItem', item);
}, },
async deleteItem({commit, getters, state}, item) { async deleteItem({commit, getters, state}, item) {
await getters.session.delete(`/2/${getters.getEventSlug}/item/${item.id}/`, item); await getters.session.delete(`/2/${getters.getEventSlug}/item/${item.uid}/`, item);
commit('removeItem', item); commit('removeItem', item);
}, },
async postItem({commit, getters, state}, item) { async postItem({commit, getters, state}, item) {

View file

@ -2,98 +2,53 @@
<AsyncLoader :loaded="!!item.id"> <AsyncLoader :loaded="!!item.id">
<div class="container-fluid px-xl-5 mt-3"> <div class="container-fluid px-xl-5 mt-3">
<div class="row"> <div class="row">
<div class="col-lg-3 col-xl-2"> <div class="col-xl-8 offset-xl-2">
<div class="card bg-dark text-light mb-2" id="filters">
<div class="card bg-dark">
<AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`"
class="d-block card-img"
@click="openLightboxModalWith(item.file)"
/>
<div class="card-body">
<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>
<div class="col-xl-8">
<div class="card bg-dark text-light mb-2" id="filters"> <div class="card bg-dark text-light mb-2" id="filters">
<div class="card-header"> <div class="card-header">
<h3>Item #{{ item.id }} - {{ item.description }}</h3> <h3>Item #{{ item.id }} - {{ item.description }}</h3>
</div> </div>
<Timeline :timeline="item.timeline"> <div>
<!--template v-slot:timeline_action1> {{ item }}
<span class="timeline-item-icon | faded-icon"> </div>
<font-awesome-icon icon="comment"/> <ItemTimeline :timeline="item.timeline" @sendMail="handleMail" @addComment="handleComment"/>
</span>
<div class="new-comment card bg-dark">
<div class="">
<textarea placeholder="add comment..." v-model="newComment"
class="form-control">
</textarea>
<AsyncButton class="btn btn-primary float-right" :task="addCommentAndClear">
<font-awesome-icon icon="comment"/>
Save Comment
</AsyncButton>
</div>
</div>
</template-->
</Timeline>
<div class="card-footer d-flex justify-content-between"> <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"> <div class="btn-group">
<button class="btn btn-outline-success" <button class="btn btn-outline-success"
@click.stop="confirm('return Item?') && markItemReturned(item)" @click.stop="confirm('return Item?') && markItemReturned(item)"
title="returned"> 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>
<button class="btn btn-outline-danger" <button class="btn btn-outline-danger"
@click.stop="confirm('delete Item?') && deleteItem(item)" @click.stop="confirm('delete Item?') && deleteItem(item)"
title="delete"> title="delete">
<font-awesome-icon icon="trash"/>&nbsp;delete <font-awesome-icon icon="trash"/>
</button> </button>
</div> </div>
<InputCombo <InputCombo
label="box" label="box"
:model="editingItem" :model="item"
nameKey="box" nameKey="box"
uniqueKey="cid" uniqueKey="cid"
:options="boxes" :options="boxes"
style="width: auto;"
/> />
<div class="btn-group">
<button type="button" class="btn btn-success" @click="saveEditingItem()">Save Changes <select class="form-control" v-model="selected_state">
</button> <option v-for="status in state_options" :value="status.value">{{
{{ editingItem}} status.text
</div> }}
</div> </option>
</div> </select>
<div class="col-lg-3 col-xl-2" v-if="item.related_issues && item.related_issues.length"> <button class="form-control btn btn-success"
<div class="card bg-dark text-light mb-2" id="filters"> @click="changeTicketStatus(item)"
<div class="card-body"> :disabled="(selected_state == item.state)">
<h5 class="card-title text-info">Related</h5> Change&nbsp;Status
</div> </button>
<div class="card bg-dark" v-for="issue in item.related_issues" v-bind:key="item.id">
<div class="card-body">
<router-link :to="{name: 'ticket', params: {id: issue.id}}">
<h6 class="card-title">Ticket #{{ issue.id }} - {{ issue.name }}</h6>
</router-link>
<h6 class="card-subtitle text-secondary">state: {{ issue.state }}</h6>
</div> </div>
</div> </div>
</div> </div>
@ -103,29 +58,22 @@
</AsyncLoader> </AsyncLoader>
</template> </template>
<script> <script>
import {mapActions, mapGetters, mapMutations, mapState} from 'vuex'; import {mapActions, mapGetters, mapState} from 'vuex';
import Timeline from "@/components/Timeline.vue"; import ItemTimeline from "@/components/ItemTimeline.vue";
import ClipboardButton from "@/components/inputs/ClipboardButton.vue"; import ClipboardButton from "@/components/inputs/ClipboardButton.vue";
import AsyncLoader from "@/components/AsyncLoader.vue"; import AsyncLoader from "@/components/AsyncLoader.vue";
import InputCombo from "@/components/inputs/InputCombo.vue"; import InputCombo from "@/components/inputs/InputCombo.vue";
import InputString from "@/components/inputs/InputString.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 { export default {
name: 'Item', name: 'Item',
components: { components: {InputString, InputCombo, AsyncLoader, ClipboardButton, ItemTimeline},
EditItem,
Modal, InputPhoto, AuthenticatedImage, InputString, InputCombo, AsyncLoader, ClipboardButton, Timeline
},
data() { data() {
return { return {
newComment: "", selected_state: null,
editingItem: {}, selected_assignee: null,
shipping_voucher_type: null,
} }
}, },
computed: { computed: {
@ -137,40 +85,45 @@ export default {
return ret ? ret : {}; return ret ? ret : {};
}, },
boxes() { boxes() {
console.log(this.getBoxes);
return this.getBoxes.map(obj => ({cid: obj.cid, box: obj.name})); return this.getBoxes.map(obj => ({cid: obj.cid, box: obj.name}));
} }
}, },
methods: { methods: {
...mapActions(['deleteItem', 'markItemReturned', 'updateTicketPartial', 'postComment']), ...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']),
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit', 'updateItem']), ...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']),
...mapActions(['claimShippingVoucher', 'fetchShippingVouchers', 'loadEventItems', 'loadBoxes']), ...mapActions(['claimShippingVoucher', 'fetchShippingVouchers', 'loadEventItems', 'loadBoxes']),
...mapMutations(['openLightboxModalWith']), handleMail(mail) {
addCommentAndClear: async function () { this.sendMail({
await this.postComment({ id: this.item.id,
id: this.ticket.id, message: mail
message: this.newComment
}) })
this.newComment = "";
}, },
closeEditingModal() { handleComment(comment) {
this.editingItem = null; this.postComment({
id: this.item.id,
message: comment
})
}, },
async saveEditingItem() { // Saves the edited copy of the item. changeTicketStatus(item) {
await this.updateItem(this.editingItem); item.state = this.selected_state;
this.editingItem = {...this.item} this.updateTicketPartial({
}, id: item.id,
storeImage(image) { state: this.selected_state,
this.item.dataImage = image; })
},
confirm(message) {
return window.confirm(message);
}, },
//assignTicket(item) {
// item.assigned_to = this.selected_assignee;
// this.updateTicketPartial({
// id: item.id,
// assigned_to: this.selected_assignee
// })
//},
}, },
mounted() { mounted() {
this.scheduleAfterInit(() => [Promise.all([this.loadEventItems(), this.loadBoxes()]).then(() => { this.scheduleAfterInit(() => [Promise.all([this.loadEventItems(), this.loadBoxes()]).then(() => {
this.selected_state = this.item.state; this.selected_state = this.item.state;
this.selected_assignee = this.item.assigned_to this.selected_assignee = this.item.assigned_to
this.editingItem = {...this.item}
})]); })]);
} }
}; };

View file

@ -1,12 +1,25 @@
<template> <template>
<AsyncLoader :loaded="isItemsLoaded"> <AsyncLoader :loaded="isItemsLoaded">
<div class="container-fluid px-xl-5 mt-3"> <div class="container-fluid px-xl-5 mt-3">
<Modal title="Edit Item" v-if="editingItem" @close="closeEditingModal()">
<template #body>
<EditItem
:item="editingItem"
badge="uid"
/>
</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="row" v-if="layout === 'table'">
<div class="col-xl-8 offset-xl-2"> <div class="col-xl-8 offset-xl-2">
<Table <Table
:columns="['id', 'description', 'box']" :columns="['uid', 'description', 'box']"
:items="getEventItems" :items="getEventItems"
:keyName="'id'" :keyName="'uid'"
@itemActivated="showItemDetail" @itemActivated="showItemDetail"
> >
<template #actions="{ item }"> <template #actions="{ item }">
@ -30,11 +43,11 @@
</div> </div>
<Cards <Cards
v-if="layout === 'cards'" v-if="layout === 'cards'"
:columns="['id', 'description', 'box']" :columns="['uid', 'description', 'box']"
:items="getEventItems" :items="getEventItems"
:keyName="'id'" :keyName="'uid'"
v-slot="{ item }" v-slot="{ item }"
@itemActivated="item => openLightboxModalWith(item.file)" @itemActivated="showItemDetail"
> >
<AuthenticatedImage v-if="item.file" cached <AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`" :src="`/media/2/256/${item.file}/`"
@ -42,14 +55,14 @@
/> />
<div class="card-body"> <div class="card-body">
<h6 class="card-title">{{ item.description }}</h6> <h6 class="card-title">{{ item.description }}</h6>
<h6 class="card-subtitle text-secondary">id: {{ item.id }} box: {{ item.box }}</h6> <h6 class="card-subtitle text-secondary">uid: {{ item.uid }} box: {{ item.box }}</h6>
<div class="row mx-auto mt-2"> <div class="row mx-auto mt-2">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-outline-success" <button class="btn btn-outline-success"
@click.stop="confirm('return Item?') && markItemReturned(item)" title="returned"> @click.stop="confirm('return Item?') && markItemReturned(item)" title="returned">
<font-awesome-icon icon="check"/> <font-awesome-icon icon="check"/>
</button> </button>
<button class="btn btn-outline-secondary" @click.stop="showItemDetail(item)" <button class="btn btn-outline-secondary" @click.stop="openEditingModalWith(item)"
title="edit"> title="edit">
<font-awesome-icon icon="edit"/> <font-awesome-icon icon="edit"/>
</button> </button>
@ -71,7 +84,8 @@ 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, mapMutations} from 'vuex'; import {mapActions, mapGetters, mapState} from 'vuex';
import Lightbox from '../components/Lightbox';
import AuthenticatedImage from "@/components/AuthenticatedImage.vue"; import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AsyncLoader from "@/components/AsyncLoader.vue"; import AsyncLoader from "@/components/AsyncLoader.vue";
import router from "@/router"; import router from "@/router";
@ -82,16 +96,32 @@ export default {
lightboxHash: null, lightboxHash: null,
editingItem: null, editingItem: null,
}), }),
components: {AsyncLoader, AuthenticatedImage, Table, Cards, Modal, EditItem}, components: {AsyncLoader, AuthenticatedImage, Lightbox, Table, Cards, Modal, EditItem},
computed: { computed: {
...mapState([]),
...mapGetters(['getEventItems', 'isItemsLoaded', 'layout']), ...mapGetters(['getEventItems', 'isItemsLoaded', 'layout']),
}, },
methods: { methods: {
...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem', 'scheduleAfterInit']), ...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem', 'scheduleAfterInit']),
...mapMutations(['openLightboxModalWith']),
showItemDetail(item) { showItemDetail(item) {
router.push({name: 'item', params: {id: item.id}}); 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) { confirm(message) {
return window.confirm(message); return window.confirm(message);
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<AsyncLoader :loaded="!!ticket.id"> <AsyncLoader :loaded="ticket.id">
<div class="container-fluid px-xl-5 mt-3"> <div class="container-fluid px-xl-5 mt-3">
<div class="row"> <div class="row">
<div class="col-xl-8 offset-xl-2"> <div class="col-xl-8 offset-xl-2">
@ -7,42 +7,7 @@
<div class="card-header"> <div class="card-header">
<h3>Ticket #{{ ticket.id }} - {{ ticket.name }}</h3> <h3>Ticket #{{ ticket.id }} - {{ ticket.name }}</h3>
</div> </div>
<Timeline :timeline="ticket.timeline"> <Timeline :timeline="ticket.timeline" @sendMail="handleMail" @addComment="handleComment"/>
<template v-slot:timeline_action1>
<span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="comment"/>
</span>
<div class="new-comment card bg-dark">
<div>
<textarea placeholder="add comment..." v-model="newComment"
class="form-control">
</textarea>
<AsyncButton class="btn btn-primary float-right" :task="addCommentAndClear">
<font-awesome-icon icon="comment"/>
Save Comment
</AsyncButton>
</div>
</div>
</template>
<template v-slot:timeline_action2>
<span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="envelope"/>
</span>
<div class="new-mail card bg-dark">
<div class="card-header">
{{ newestMailSubject }}
</div>
<div>
<textarea placeholder="reply mail..." v-model="newMail" class="form-control">
</textarea>
<AsyncButton class="btn btn-primary float-right" :task="sendMailAndClear">
<font-awesome-icon icon="envelope"/>
Send Mail
</AsyncButton>
</div>
</div>
</template>
</Timeline>
<div class="card-footer d-flex justify-content-between"> <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>
<!--button class="btn btn-danger" @click="deleteItem({type: 'tickets', id: ticket.id})"> <!--button class="btn btn-danger" @click="deleteItem({type: 'tickets', id: ticket.id})">
@ -51,14 +16,11 @@
</button--> </button-->
<div class="btn-group"> <div class="btn-group">
<select class="form-control" v-model="selected_assignee"> <select class="form-control" v-model="selected_assignee">
<option v-for="user in users" :value="user.username">{{ <option v-for="user in users" :value="user.username">{{ user.username }}</option>
user.username
}}
</option>
</select> </select>
<button class="form-control btn btn-success" <button class="form-control btn btn-success"
@click="assignTicket(ticket)" @click="assignTicket(ticket)"
:disabled="!selected_assignee || (selected_assignee === ticket.assigned_to)"> :disabled="!selected_assignee || (selected_assignee == ticket.assigned_to)">
Assign&nbsp;Ticket Assign&nbsp;Ticket
</button> </button>
</div> </div>
@ -71,7 +33,7 @@
</select> </select>
<button class="form-control btn btn-success" <button class="form-control btn btn-success"
@click="changeTicketStatus(ticket)" @click="changeTicketStatus(ticket)"
:disabled="(selected_state === ticket.state)"> :disabled="(selected_state == ticket.state)">
Change&nbsp;Status Change&nbsp;Status
</button> </button>
</div> </div>
@ -96,53 +58,25 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-3 col-xl-2 d-lg-none d-xl-block"
v-if="ticket.related_items && ticket.related_items.length">
<div class="card bg-dark text-light mb-2" id="filters">
<div class="card-body">
<h5 class="card-title text-info">Related</h5>
<div class="card bg-dark" v-for="item in ticket.related_items" v-bind:key="item.id">
<AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`"
class="d-block card-img"
@click="openLightboxModalWith(item.file)"
/>
<div class="card-body">
<!--h6 class="card-title text-info"><span class="badge badge-primary">{{ item.relation_status }}</span></--h6-->
<h6 class="card-subtitle text-secondary">id: {{ item.id }} box: {{
item.box
}}</h6>
<router-link :to="{name: 'item', params: {id: item.id}}">
<h6 class="card-title">{{ item.description }}</h6>
</router-link>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</AsyncLoader> </AsyncLoader>
</template> </template>
<script> <script>
import {mapActions, mapGetters, mapMutations, mapState} from 'vuex'; import {mapActions, mapGetters, mapState} from 'vuex';
import Timeline from "@/components/Timeline.vue"; import Timeline from "@/components/Timeline.vue";
import ClipboardButton from "@/components/inputs/ClipboardButton.vue"; import ClipboardButton from "@/components/inputs/ClipboardButton.vue";
import AsyncLoader from "@/components/AsyncLoader.vue"; import AsyncLoader from "@/components/AsyncLoader.vue";
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AsyncButton from "@/components/inputs/AsyncButton.vue";
export default { export default {
name: 'Ticket', name: 'Ticket',
components: {AsyncButton, AuthenticatedImage, AsyncLoader, ClipboardButton, Timeline}, components: {AsyncLoader, ClipboardButton, Timeline},
data() { data() {
return { return {
selected_state: null, selected_state: null,
selected_assignee: null, selected_assignee: null,
shipping_voucher_type: null, shipping_voucher_type: null,
newMail: "",
newComment: ""
} }
}, },
computed: { computed: {
@ -156,52 +90,46 @@ export default {
shippingEmail() { shippingEmail() {
const domain = document.location.hostname; const domain = document.location.hostname;
return `ticket+${this.ticket.uuid}@${domain}`; return `ticket+${this.ticket.uuid}@${domain}`;
}, }
newestMailSubject() {
const mail = this.ticket.timeline ? this.ticket.timeline.filter(item => item.type === 'mail').pop() : null;
return mail ? mail.subject : "";
},
}, },
methods: { methods: {
...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']), ...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']),
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']), ...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']),
...mapActions(['claimShippingVoucher', 'fetchShippingVouchers']), ...mapActions(['claimShippingVoucher', 'fetchShippingVouchers']),
...mapMutations(['openLightboxModalWith']), handleMail(mail) {
changeTicketStatus() { this.sendMail({
this.ticket.state = this.selected_state;
this.updateTicketPartial({
id: this.ticket.id, id: this.ticket.id,
message: mail
})
},
handleComment(comment) {
this.postComment({
id: this.ticket.id,
message: comment
})
},
changeTicketStatus(ticket) {
ticket.state = this.selected_state;
this.updateTicketPartial({
id: ticket.id,
state: this.selected_state, state: this.selected_state,
}) })
}, },
assignTicket() { assignTicket(ticket) {
this.ticket.assigned_to = this.selected_assignee; ticket.assigned_to = this.selected_assignee;
this.updateTicketPartial({ this.updateTicketPartial({
id: this.ticket.id, id: ticket.id,
assigned_to: this.selected_assignee assigned_to: this.selected_assignee
}) })
}, },
sendMailAndClear: async function () {
await this.sendMail({
id: this.ticket.id,
message: this.newMail,
})
this.newMail = "";
},
addCommentAndClear: async function () {
await this.postComment({
id: this.ticket.id,
message: this.newComment
})
this.newComment = "";
}
}, },
mounted() { mounted() {
this.scheduleAfterInit(() => [Promise.all([this.fetchTicketStates(), this.loadTickets(), this.loadUsers(), this.fetchShippingVouchers()]).then(() => { this.scheduleAfterInit(() => [Promise.all([this.fetchTicketStates(), this.loadTickets(), this.loadUsers(), this.fetchShippingVouchers()]).then(() => {
if (this.ticket.state === "pending_new") { if (this.ticket.state == "pending_new") {
this.selected_state = "pending_open"; this.selected_state = "pending_open";
this.changeTicketStatus() this.changeTicketStatus(this.ticket)
} }
;
this.selected_state = this.ticket.state; this.selected_state = this.ticket.state;
this.selected_assignee = this.ticket.assigned_to this.selected_assignee = this.ticket.assigned_to
})]); })]);

View file

@ -12,11 +12,11 @@
> >
<template v-slot:actions="{item}"> <template v-slot:actions="{item}">
<div class="btn-group"> <div class="btn-group">
<router-link :to="{name: 'ticket', params: {id: item.id}}" class="btn btn-primary" <a class="btn btn-primary" :href="'/'+ getEventSlug + '/ticket/' + item.id" title="view"
title="view"> @click.prevent="gotoDetail(item)">
<font-awesome-icon icon="eye"/> <font-awesome-icon icon="eye"/>
View View
</router-link> </a>
</div> </div>
</template> </template>
</SlotTable> </SlotTable>
@ -39,11 +39,11 @@
<td v-if="getEventSlug==='all'">{{ item.event }}</td> <td v-if="getEventSlug==='all'">{{ item.event }}</td>
<td> <td>
<div class="btn-group"> <div class="btn-group">
<router-link :to="{name: 'ticket', params: {id: item.id}}" class="btn btn-primary" <a class="btn btn-primary" :href="'/'+ getEventSlug + '/ticket/' + item.id" title="view"
title="view"> @click.prevent="gotoDetail(item)">
<font-awesome-icon icon="eye"/> <font-awesome-icon icon="eye"/>
View View
</router-link> </a>
</div> </div>
</td> </td>
</tr> </tr>
@ -55,23 +55,24 @@
<script> <script>
import Cards from '@/components/Cards'; import Cards from '@/components/Cards';
import Modal from '@/components/Modal';
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 SlotTable from "@/components/SlotTable.vue"; import SlotTable from "@/components/SlotTable.vue";
import CollapsableCards from "@/components/CollapsableCards.vue"; import CollapsableCards from "@/components/CollapsableCards.vue";
import AsyncLoader from "@/components/AsyncLoader.vue"; import AsyncLoader from "@/components/AsyncLoader.vue";
import router from "@/router";
export default { export default {
name: 'Tickets', name: 'Tickets',
components: {AsyncLoader, Lightbox, SlotTable, Cards, CollapsableCards}, components: {AsyncLoader, Lightbox, SlotTable, Cards, Modal, EditItem, CollapsableCards},
computed: { computed: {
...mapGetters(['getEventTickets', 'isTicketsLoaded', 'stateInfo', 'getEventSlug', 'layout']), ...mapGetters(['getEventTickets', 'isTicketsLoaded', 'stateInfo', 'getEventSlug', 'layout']),
}, },
methods: { methods: {
...mapActions(['loadTickets', 'fetchTicketStates', 'scheduleAfterInit']), ...mapActions(['loadTickets', 'fetchTicketStates', 'scheduleAfterInit']),
gotoDetail(ticket) { gotoDetail(ticket) {
router.push({name: 'ticket', params: {id: ticket.id}}); this.$router.push({name: 'ticket', params: {id: ticket.id}});
}, },
formatTicket(ticket) { formatTicket(ticket) {
return { return {

View file

@ -88,11 +88,11 @@ export default {
if (!this.events[id].addresses.includes(a)) if (!this.events[id].addresses.includes(a))
this.events[id].addresses.push(a) this.events[id].addresses.push(a)
this.new_address[id] = "" this.new_address[id] = ""
this.updateEvent({id: this.events[id].id, partial_event: {addresses: this.events[id].addresses}}); this.updateEvent({id: this.events[id].eid, partial_event: {addresses: this.events[id].addresses}});
}, },
deleteAddress(id, a_id) { deleteAddress(id, a_id) {
this.events[id].addresses = this.events[id].addresses.filter((e, i) => i !== a_id); this.events[id].addresses = this.events[id].addresses.filter((e, i) => i !== a_id);
this.updateEvent({id: this.events[id].id, partial_event: {addresses: this.events[id].addresses}}); this.updateEvent({id: this.events[id].eid, partial_event: {addresses: this.events[id].addresses}});
} }
}, },
}; };