From f0f0a93e0b0a2641f0fa929d768b2b7ddd88481e Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Wed, 18 Feb 2026 03:36:56 +0100 Subject: [PATCH] fixed ticket view --- backend/api/models.py | 2 + backend/api/serializers.py | 14 +++- backend/api/views.py | 33 +++++++++- frontend/src/pages/AdminDashboard.tsx | 9 +++ frontend/src/pages/AdminUserDetail.tsx | 2 +- frontend/src/pages/Support.tsx | 88 ++++++++++++++++++-------- 6 files changed, 119 insertions(+), 29 deletions(-) diff --git a/backend/api/models.py b/backend/api/models.py index 0d77719..e05f5df 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -124,12 +124,14 @@ class Notification(models.Model): TYPE_CHOICES = [ ('reply', 'Reply'), ('mention', 'Mention'), + ('ticket', 'Ticket Update'), ] recipient = models.ForeignKey(User, related_name='notifications', on_delete=models.CASCADE) actor = models.ForeignKey(User, related_name='actions', on_delete=models.CASCADE) notification_type = models.CharField(max_length=10, choices=TYPE_CHOICES) rating = models.ForeignKey(Rating, on_delete=models.CASCADE, null=True, blank=True) reply = models.ForeignKey(Reply, on_delete=models.CASCADE, null=True, blank=True) + ticket = models.ForeignKey('Ticket', on_delete=models.CASCADE, null=True, blank=True) is_read = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 854e3c6..188df41 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -119,13 +119,23 @@ class TicketMessageSerializer(serializers.ModelSerializer): class TicketSerializer(serializers.ModelSerializer): username = serializers.CharField(source='user.username', read_only=True) + user_email = serializers.EmailField(source='user.email', read_only=True) + user_avatar = serializers.SerializerMethodField() messages = TicketMessageSerializer(many=True, read_only=True) class Meta: model = Ticket - fields = ['id', 'user', 'username', 'subject', 'description', 'status', 'created_at', 'updated_at', 'messages'] + fields = ['id', 'user', 'username', 'user_email', 'user_avatar', 'subject', 'description', 'status', 'created_at', 'updated_at', 'messages'] read_only_fields = ['status', 'user'] + def get_user_avatar(self, obj): + if obj.user.avatar: + request = self.context.get('request') + if request: + return request.build_absolute_uri(obj.user.avatar.url) + return obj.user.avatar.url + return None + class AdminUserListSerializer(serializers.ModelSerializer): ips = serializers.SerializerMethodField() @@ -155,7 +165,7 @@ class NotificationSerializer(serializers.ModelSerializer): class Meta: model = Notification - fields = ['id', 'actor_username', 'actor_avatar', 'notification_type', 'rating', 'reply', 'is_read', 'created_at', 'flavor_name', 'flavor_id'] + fields = ['id', 'actor_username', 'actor_avatar', 'notification_type', 'rating', 'reply', 'ticket', 'is_read', 'created_at', 'flavor_name', 'flavor_id'] def get_actor_avatar(self, obj): if obj.actor.avatar: diff --git a/backend/api/views.py b/backend/api/views.py index 7ffc6cc..9c2106e 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -576,7 +576,16 @@ class TicketViewSet(viewsets.ModelViewSet): return Ticket.objects.filter(user=self.request.user).prefetch_related('messages', 'messages__user').order_by('-updated_at') def perform_create(self, serializer): - serializer.save(user=self.request.user) + ticket = serializer.save(user=self.request.user) + # Notify all superusers about new ticket + admins = User.objects.filter(is_superuser=True).exclude(pk=self.request.user.pk) + for admin in admins: + Notification.objects.create( + recipient=admin, + actor=self.request.user, + notification_type='ticket', + ticket=ticket + ) @action(detail=True, methods=['post']) def add_message(self, request, pk=None): @@ -586,6 +595,28 @@ class TicketViewSet(viewsets.ModelViewSet): return Response({'error': 'Text is required'}, status=status.HTTP_400_BAD_REQUEST) msg = TicketMessage.objects.create(ticket=ticket, user=request.user, text=text) + + # Notification logic + if request.user.is_superuser: + # Notify the user who opened the ticket + if ticket.user != request.user: + Notification.objects.create( + recipient=ticket.user, + actor=request.user, + notification_type='ticket', + ticket=ticket + ) + else: + # Notify all superusers + admins = User.objects.filter(is_superuser=True).exclude(pk=request.user.pk) + for admin in admins: + Notification.objects.create( + recipient=admin, + actor=request.user, + notification_type='ticket', + ticket=ticket + ) + return Response(TicketMessageSerializer(msg).data, status=status.HTTP_201_CREATED) @action(detail=True, methods=['post'], permission_classes=[permissions.IsAdminUser]) diff --git a/frontend/src/pages/AdminDashboard.tsx b/frontend/src/pages/AdminDashboard.tsx index af79e0a..0846138 100644 --- a/frontend/src/pages/AdminDashboard.tsx +++ b/frontend/src/pages/AdminDashboard.tsx @@ -7,6 +7,7 @@ import { } from '@mui/material'; import EditIcon from '@mui/icons-material/Edit'; import EmailIcon from '@mui/icons-material/Email'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import api from '../api'; @@ -86,6 +87,14 @@ const AdminDashboard: React.FC = () => { return ( + {t('admin.title')} diff --git a/frontend/src/pages/AdminUserDetail.tsx b/frontend/src/pages/AdminUserDetail.tsx index 355b4df..7e2f41f 100644 --- a/frontend/src/pages/AdminUserDetail.tsx +++ b/frontend/src/pages/AdminUserDetail.tsx @@ -121,7 +121,7 @@ const AdminUserDetail: React.FC = () => { {t('support.title')} + + + )} alpha(theme.palette.background.default, 0.5) }}> {currentUser?.is_superuser && ( - + {['open', 'in_progress', 'resolved', 'closed'].map((s) => ( @@ -208,7 +244,7 @@ const Support: React.FC = () => { )} - + {ticket.description} @@ -216,17 +252,18 @@ const Support: React.FC = () => { {ticket.messages.map((msg: Message) => ( {msg.username} {msg.is_admin && '(Support)'} - {msg.text} - {formatDate(msg.created_at)} + {msg.text} + {formatDate(msg.created_at)} ))} @@ -237,6 +274,7 @@ const Support: React.FC = () => { fullWidth size="small" placeholder={t('community.writeReply')} value={replyText[ticket.id] || ''} onChange={(e) => setReplyText({ ...replyText, [ticket.id]: e.target.value })} + onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleReply(ticket.id)} /> handleReply(ticket.id)} disabled={!replyText[ticket.id]}>