Compare commits
21 Commits
6dd1a7e1c3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| f8050cc32d | |||
| e1859f8f29 | |||
| f92d34b0a0 | |||
| 65360a95e5 | |||
| 71b834393d | |||
| 05f4102b81 | |||
| 2fb8435600 | |||
| db9fab89e5 | |||
| d282f39ac1 | |||
| 349ccabd74 | |||
| 272415d219 | |||
| 8af0bbdb37 | |||
| 61a7ca6403 | |||
| 35d8727348 | |||
| 1388166e9d | |||
| af8176ac48 | |||
| 45b7f91f13 | |||
| 7ad635dc12 | |||
| 190dab5427 | |||
| 3e17050c40 | |||
| 63a66f6d3e |
37
README.md
37
README.md
@@ -2,10 +2,43 @@
|
||||
|
||||
Die Webanwendung unterstützt das Korrekturmanagement von Materialien aus dem Fernstudium
|
||||
|
||||
## Live-Demo
|
||||
Eine Testumgebung der aktuellen Version wird unter folgender URL bereitgestellt:
|
||||
### [studybug.de](https://studybug.de)
|
||||
|
||||
## Installation
|
||||
|
||||
Use the package manager [pip](https://pip.pypa.io/en/stable/) to install django.
|
||||
```bash
|
||||
git clone https://git.paul.nrw/Paul/korrekturmanagementsystem.git
|
||||
cd ticketsystem
|
||||
```
|
||||
|
||||
Virtual environment erstellen und aktivieren
|
||||
```bash
|
||||
python -m venv venv
|
||||
# Windows
|
||||
venv\Scripts\activate
|
||||
# macOS / Linux
|
||||
source venv/bin/activate
|
||||
```
|
||||
Abhängigkeiten installieren
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Datenbank einrichten
|
||||
|
||||
```bash
|
||||
pip install django
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
Entwicklungsserver starten
|
||||
```bash
|
||||
python manage.py runserver
|
||||
```
|
||||
Tailwindcss CLI installieren und CSS generieren
|
||||
```bash
|
||||
npm install tailwindcss @tailwindcss/cli
|
||||
npx @tailwindcss/cli -i ticketsystem/static/css/style.css -o ticketsystem/static/css/tailwind.css
|
||||
```
|
||||
@@ -126,5 +126,5 @@ STATICFILES_DIRS = [BASE_DIR / "static"]
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
LOGIN_REDIRECT_URL = "/ticketsystem"
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
LOGOUT_REDIRECT_URL = "/accounts/login/"
|
||||
|
||||
@@ -19,7 +19,7 @@ from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path("ticketsystem/", include("ticketsystem.urls")),
|
||||
path("", include("ticketsystem.urls")),
|
||||
path("admin/", admin.site.urls),
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
]
|
||||
|
||||
32
requirements.txt
Normal file
32
requirements.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
asgiref==3.8.1
|
||||
astroid==3.3.10
|
||||
black==25.1.0
|
||||
chardet==5.2.0
|
||||
click==8.1.8
|
||||
colorama==0.4.6
|
||||
cssbeautifier==1.15.4
|
||||
dill==0.4.0
|
||||
Django==5.2
|
||||
djlint==1.36.4
|
||||
EditorConfig==0.17.0
|
||||
gunicorn==23.0.0
|
||||
isort==6.0.1
|
||||
jsbeautifier==1.15.4
|
||||
json5==0.12.0
|
||||
mccabe==0.7.0
|
||||
mypy_extensions==1.1.0
|
||||
packaging==25.0
|
||||
pathspec==0.12.1
|
||||
pillow==11.2.1
|
||||
platformdirs==4.3.7
|
||||
pylint==3.3.7
|
||||
pylint-django==2.6.1
|
||||
pylint-plugin-utils==0.8.2
|
||||
PyYAML==6.0.2
|
||||
regex==2024.11.6
|
||||
reportlab==4.4.1
|
||||
six==1.17.0
|
||||
sqlparse==0.5.3
|
||||
tomlkit==0.13.3
|
||||
tqdm==4.67.1
|
||||
tzdata==2025.2
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Ticket, Course, FAQ
|
||||
from .models import FAQ, Course, Ticket
|
||||
|
||||
admin.site.register(Ticket)
|
||||
admin.site.register(Course)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from django import forms
|
||||
from .models import Comment, Ticket
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from .models import Comment, Ticket
|
||||
|
||||
|
||||
class CommentForm(forms.ModelForm):
|
||||
class Meta:
|
||||
@@ -17,38 +18,55 @@ class CommentForm(forms.ModelForm):
|
||||
class TicketForm(forms.ModelForm):
|
||||
# Zentrale Definition der Status-Übergänge
|
||||
STATUS_TRANSITIONS = {
|
||||
'tutor': {
|
||||
'new': ['in_progress'],
|
||||
'in_progress': ['resolved', 'new'],
|
||||
'resolved': ['closed'],
|
||||
'closed': [],
|
||||
"tutor": {
|
||||
"new": ["in_progress"],
|
||||
"in_progress": ["resolved", "new"],
|
||||
"resolved": ["closed"],
|
||||
"closed": [],
|
||||
},
|
||||
"creator": {
|
||||
"new": [],
|
||||
"in_progress": [],
|
||||
"resolved": ["closed", "new"],
|
||||
"closed": [],
|
||||
},
|
||||
"superuser": {
|
||||
# Superuser können alle Übergänge machen
|
||||
"new": ["in_progress", "resolved", "closed"],
|
||||
"in_progress": ["new", "resolved", "closed"],
|
||||
"resolved": ["new", "in_progress", "closed"],
|
||||
"closed": ["new", "in_progress", "resolved"],
|
||||
},
|
||||
'creator': {
|
||||
'new': [],
|
||||
'in_progress': [],
|
||||
'resolved': ['closed', 'new'],
|
||||
'closed': [],
|
||||
}
|
||||
}
|
||||
|
||||
# Zentrale Definition welche Felder wann required sind
|
||||
REQUIRED_FIELDS_BY_STATUS = {
|
||||
'resolved': ['answer'], # Answer required when resolving
|
||||
"resolved": ["answer"], # Answer required when resolving
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = ["title", "description", "status", "mistake", "course", "answer", "material"]
|
||||
fields = [
|
||||
"title",
|
||||
"description",
|
||||
"status",
|
||||
"mistake",
|
||||
"course",
|
||||
"answer",
|
||||
"material",
|
||||
]
|
||||
widgets = {
|
||||
'answer': forms.Textarea(attrs={
|
||||
'rows': 4,
|
||||
'placeholder': 'Beschreibe die Lösung des Problems...'
|
||||
})
|
||||
"answer": forms.Textarea(
|
||||
attrs={
|
||||
"rows": 4,
|
||||
"placeholder": "Beschreibe die Lösung des Problems...",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop('user', None)
|
||||
self.ticket = kwargs.pop('ticket', None)
|
||||
self.user = kwargs.pop("user", None)
|
||||
self.ticket = kwargs.pop("ticket", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.ticket and self.user:
|
||||
@@ -61,35 +79,42 @@ class TicketForm(forms.ModelForm):
|
||||
is_superuser = self.user.is_superuser
|
||||
|
||||
# Status-Choices einschränken
|
||||
self._limit_status_choices(is_tutor, is_creator)
|
||||
self._limit_status_choices(is_tutor, is_creator, is_superuser)
|
||||
|
||||
# Feld-Berechtigungen setzen
|
||||
self._set_field_permissions(is_tutor, is_creator, is_superuser)
|
||||
|
||||
def _set_field_permissions(self, is_tutor, is_creator, is_superuser):
|
||||
"""Setzt welche Felder bearbeitet werden dürfen"""
|
||||
if self.ticket.status == 'resolved' and is_creator and not is_superuser:
|
||||
# Superuser können alles bearbeiten
|
||||
if is_superuser:
|
||||
return
|
||||
|
||||
if self.ticket.status == "resolved" and is_creator:
|
||||
for field_name in self.fields:
|
||||
if field_name == "answer":
|
||||
self.fields[field_name].disabled = True
|
||||
elif is_tutor and not is_superuser:
|
||||
elif is_tutor:
|
||||
# Tutor darf ändern:
|
||||
readonly_fields = ['title', 'description', 'material']
|
||||
readonly_fields = ["title", "description", "material"]
|
||||
for field_name in readonly_fields:
|
||||
if field_name in self.fields:
|
||||
self.fields[field_name].disabled = True
|
||||
elif is_creator and not is_superuser and self.ticket.status != 'resolved':
|
||||
elif is_creator and self.ticket.status != "resolved":
|
||||
for field_name in self.fields:
|
||||
self.fields[field_name].disabled = True
|
||||
|
||||
def _limit_status_choices(self, is_tutor, is_creator):
|
||||
def _limit_status_choices(self, is_tutor, is_creator, is_superuser):
|
||||
"""Beschränkt verfügbare Status-Optionen basierend auf der zentralen Logik"""
|
||||
current_status = self.ticket.status
|
||||
|
||||
if is_tutor:
|
||||
role = 'tutor'
|
||||
# Superuser bekommen alle Status-Optionen
|
||||
if is_superuser:
|
||||
role = "superuser"
|
||||
elif is_tutor:
|
||||
role = "tutor"
|
||||
elif is_creator:
|
||||
role = 'creator'
|
||||
role = "creator"
|
||||
else:
|
||||
role = None
|
||||
|
||||
@@ -100,10 +125,9 @@ class TicketForm(forms.ModelForm):
|
||||
allowed_statuses = [current_status]
|
||||
|
||||
# Status-Choices filtern
|
||||
all_choices = list(self.fields['status'].choices)
|
||||
self.fields['status'].choices = [
|
||||
choice for choice in all_choices
|
||||
if choice[0] in allowed_statuses
|
||||
all_choices = list(self.fields["status"].choices)
|
||||
self.fields["status"].choices = [
|
||||
choice for choice in all_choices if choice[0] in allowed_statuses
|
||||
]
|
||||
|
||||
def _get_allowed_transitions(self, from_status, role):
|
||||
@@ -123,7 +147,7 @@ class TicketForm(forms.ModelForm):
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
status = cleaned_data.get('status')
|
||||
status = cleaned_data.get("status")
|
||||
|
||||
if not self.ticket or not status:
|
||||
return cleaned_data
|
||||
@@ -132,27 +156,35 @@ class TicketForm(forms.ModelForm):
|
||||
old_status = self.ticket.status
|
||||
is_tutor = self.user == self.ticket.assigned_to
|
||||
is_creator = self.user == self.ticket.created_by
|
||||
is_superuser = self.user.is_superuser
|
||||
|
||||
if is_tutor:
|
||||
role = 'tutor'
|
||||
# Superuser dürfen alle Übergänge
|
||||
if is_superuser:
|
||||
role = "superuser"
|
||||
elif is_tutor:
|
||||
role = "tutor"
|
||||
elif is_creator:
|
||||
role = 'creator'
|
||||
role = "creator"
|
||||
else:
|
||||
role = None
|
||||
|
||||
if role and not self._is_transition_allowed(old_status, status, role):
|
||||
raise ValidationError({
|
||||
'status': f'Übergang von "{self.ticket.get_status_display()}" zu "{dict(self.fields["status"].choices)[status]}" ist nicht erlaubt.'
|
||||
})
|
||||
raise ValidationError(
|
||||
{
|
||||
"status": f'Übergang von "{self.ticket.get_status_display()}" zu "{dict(self.fields["status"].choices)[status]}" ist nicht erlaubt.'
|
||||
}
|
||||
)
|
||||
|
||||
# Prüfe required fields für neuen Status
|
||||
required_fields = self._get_required_fields_for_status(status)
|
||||
for field_name in required_fields:
|
||||
if not cleaned_data.get(field_name):
|
||||
field_label = self.fields[field_name].label
|
||||
raise ValidationError({
|
||||
field_name: f'{field_label} ist erforderlich, wenn der Status auf "{dict(self.fields["status"].choices)[status]}" gesetzt wird.'
|
||||
})
|
||||
raise ValidationError(
|
||||
{
|
||||
field_name: f'{field_label} ist erforderlich, wenn der Status auf "{dict(self.fields["status"].choices)[status]}" gesetzt wird.'
|
||||
}
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
@@ -166,6 +198,7 @@ class TicketForm(forms.ModelForm):
|
||||
# Setze answered_at wenn eine Antwort gegeben wird
|
||||
if ticket.answer and not ticket.answered_at:
|
||||
from django.utils import timezone
|
||||
|
||||
ticket.answered_at = timezone.now()
|
||||
|
||||
if commit:
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Course(models.Model):
|
||||
"""Kurs-Model für Backend-Verwaltung"""
|
||||
|
||||
name = models.CharField(max_length=200, verbose_name="Kurs-Name")
|
||||
code = models.CharField(max_length=50, unique=True, verbose_name="Kurs-Code") # z.B. "PROG-101"
|
||||
code = models.CharField(
|
||||
max_length=50, unique=True, verbose_name="Kurs-Code"
|
||||
) # z.B. "PROG-101"
|
||||
description = models.TextField(blank=True, verbose_name="Beschreibung")
|
||||
|
||||
tutor = models.ForeignKey(
|
||||
@@ -14,14 +17,14 @@ class Course(models.Model):
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Tutor",
|
||||
related_name="courses_as_tutor"
|
||||
related_name="courses_as_tutor",
|
||||
)
|
||||
|
||||
is_active = models.BooleanField(default=True, verbose_name="Aktiv")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
ordering = ["name"]
|
||||
verbose_name = "Kurs"
|
||||
verbose_name_plural = "Kurse"
|
||||
|
||||
@@ -50,6 +53,7 @@ class Ticket(models.Model):
|
||||
]
|
||||
|
||||
MATERIAL_CHOICES = [
|
||||
("script", "Skript"),
|
||||
("learning_sprint", "Learning Sprint"),
|
||||
("ilse", "Intensive Live Session"),
|
||||
("video", "Video"),
|
||||
@@ -63,37 +67,40 @@ class Ticket(models.Model):
|
||||
description = models.TextField()
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="new")
|
||||
mistake = models.CharField(max_length=20, choices=MISTAKE_CHOICES, default="medium")
|
||||
material = models.CharField(max_length=20, choices=MATERIAL_CHOICES, default="script")
|
||||
material = models.CharField(
|
||||
max_length=20, choices=MATERIAL_CHOICES, default="script"
|
||||
)
|
||||
|
||||
answer = models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Antwort/Lösung",
|
||||
help_text="Beschreibung der Lösung (erforderlich bei Status 'Gelöst')"
|
||||
help_text="Beschreibung der Lösung (erforderlich bei Status 'Gelöst')",
|
||||
)
|
||||
answered_at = models.DateTimeField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Beantwortet am"
|
||||
blank=True, null=True, verbose_name="Beantwortet am"
|
||||
)
|
||||
|
||||
course = models.ForeignKey(
|
||||
Course,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Kurs",
|
||||
related_name="tickets"
|
||||
Course, on_delete=models.CASCADE, verbose_name="Kurs", related_name="tickets"
|
||||
)
|
||||
created_by = models.ForeignKey(
|
||||
User, related_name="tickets_created", on_delete=models.CASCADE
|
||||
)
|
||||
assigned_to = models.ForeignKey(
|
||||
User, related_name="tickets_assigned", null=True, blank=True, on_delete=models.CASCADE,
|
||||
User,
|
||||
related_name="tickets_assigned",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.get_mistake_display()}] {self.title} ({self.get_status_display()})"
|
||||
return (
|
||||
f"[{self.get_mistake_display()}] {self.title} ({self.get_status_display()})"
|
||||
)
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
@@ -115,7 +122,7 @@ class TicketHistory(models.Model):
|
||||
changed_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
changed_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
field = models.CharField(max_length=100) # z. B. "status" oder "description"
|
||||
field = models.CharField(max_length=100) # z.B. "status" oder "description"
|
||||
old_value = models.TextField(null=True, blank=True)
|
||||
new_value = models.TextField(null=True, blank=True)
|
||||
|
||||
@@ -125,13 +132,14 @@ class TicketHistory(models.Model):
|
||||
|
||||
class FAQ(models.Model):
|
||||
"""Einfaches FAQ Model"""
|
||||
|
||||
question = models.CharField(max_length=300, verbose_name="Frage")
|
||||
answer = models.TextField(verbose_name="Antwort")
|
||||
order = models.IntegerField(default=0, verbose_name="Reihenfolge")
|
||||
is_active = models.BooleanField(default=True, verbose_name="Aktiv")
|
||||
|
||||
class Meta:
|
||||
ordering = ['order', 'question']
|
||||
ordering = ["order", "question"]
|
||||
verbose_name = "FAQ"
|
||||
verbose_name_plural = "FAQs"
|
||||
|
||||
|
||||
BIN
ticketsystem/static/IU-logo.png
Normal file
BIN
ticketsystem/static/IU-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
33
ticketsystem/static/js/faq.js
Normal file
33
ticketsystem/static/js/faq.js
Normal file
@@ -0,0 +1,33 @@
|
||||
function toggleFAQ(index) {
|
||||
const content = document.getElementById(`content-${index}`);
|
||||
const icon = document.getElementById(`icon-${index}`);
|
||||
|
||||
if (content.classList.contains('hidden')) {
|
||||
content.classList.remove('hidden');
|
||||
icon.classList.add('rotate-180');
|
||||
} else {
|
||||
content.classList.add('hidden');
|
||||
icon.classList.remove('rotate-180');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAllFAQs() {
|
||||
const button = document.getElementById('toggle-all-btn');
|
||||
const allContents = document.querySelectorAll('[id^="content-"]');
|
||||
const allIcons = document.querySelectorAll('[id^="icon-"]');
|
||||
|
||||
// Prüfen ob alle eingeklappt sind
|
||||
const allCollapsed = Array.from(allContents).every(content => content.classList.contains('hidden'));
|
||||
|
||||
if (allCollapsed) {
|
||||
// Alle ausklappen
|
||||
allContents.forEach(content => content.classList.remove('hidden'));
|
||||
allIcons.forEach(icon => icon.classList.add('rotate-180'));
|
||||
button.innerHTML = '📁 Alle einklappen';
|
||||
} else {
|
||||
// Alle einklappen
|
||||
allContents.forEach(content => content.classList.add('hidden'));
|
||||
allIcons.forEach(icon => icon.classList.remove('rotate-180'));
|
||||
button.innerHTML = '📂 Alle ausklappen';
|
||||
}
|
||||
}
|
||||
83
ticketsystem/static/js/ticket_detail.js
Normal file
83
ticketsystem/static/js/ticket_detail.js
Normal file
@@ -0,0 +1,83 @@
|
||||
function initializeTicketDetail(config) {
|
||||
// Course-Tutor Mapping
|
||||
if (config.canEdit) {
|
||||
const courseSelect = document.getElementById('id_course');
|
||||
const tutorText = document.getElementById('tutor_text');
|
||||
const tutorDisplay = document.getElementById('tutor_display');
|
||||
|
||||
if (courseSelect && config.courseTutorMap) {
|
||||
courseSelect.addEventListener('change', function() {
|
||||
const selectedCourseId = this.value;
|
||||
|
||||
if (selectedCourseId && config.courseTutorMap[selectedCourseId]) {
|
||||
tutorText.textContent = config.courseTutorMap[selectedCourseId];
|
||||
tutorDisplay.classList.remove('bg-gray-100');
|
||||
tutorDisplay.classList.add('bg-blue-50');
|
||||
} else if (selectedCourseId) {
|
||||
tutorText.textContent = 'Kein Tutor zugewiesen';
|
||||
tutorDisplay.classList.remove('bg-blue-50');
|
||||
tutorDisplay.classList.add('bg-gray-100');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Answer Field Toggle (nur für Tutoren)
|
||||
if (config.canEdit && !config.isCreator) {
|
||||
const statusSelect = document.querySelector('select[name="status"]');
|
||||
const answerField = document.querySelector('textarea[name="answer"]');
|
||||
const answerLabel = answerField?.previousElementSibling;
|
||||
|
||||
function toggleAnswerField() {
|
||||
if (!answerField) return;
|
||||
|
||||
if (statusSelect.value === 'resolved') {
|
||||
// Antwort-Feld einblenden und aktivieren
|
||||
answerField.closest('div').style.display = 'block';
|
||||
answerField.disabled = false;
|
||||
answerField.readOnly = false;
|
||||
answerField.style.pointerEvents = 'auto'; // Erlaube Klicks
|
||||
answerField.classList.remove('bg-gray-100', 'cursor-not-allowed');
|
||||
answerField.classList.add('bg-white');
|
||||
answerField.required = true;
|
||||
if (answerLabel && !answerLabel.querySelector('.text-red-500')) {
|
||||
answerLabel.innerHTML = answerLabel.textContent + ' <span class="text-red-500">*</span>';
|
||||
}
|
||||
} else if (statusSelect.value === 'closed') {
|
||||
// Antwort-Feld einblenden aber ausgegraut (readonly statt disabled)
|
||||
answerField.closest('div').style.display = 'block';
|
||||
answerField.disabled = false;
|
||||
answerField.readOnly = true;
|
||||
answerField.style.pointerEvents = 'none'; // Verhindert Klicks
|
||||
answerField.classList.add('bg-gray-100', 'cursor-not-allowed');
|
||||
answerField.classList.remove('bg-white');
|
||||
answerField.required = false;
|
||||
|
||||
if (answerLabel) {
|
||||
answerLabel.innerHTML = answerLabel.textContent.replace(' *', '');
|
||||
}
|
||||
} else {
|
||||
// Antwort-Feld ausblenden
|
||||
answerField.closest('div').style.display = 'none';
|
||||
answerField.required = false;
|
||||
|
||||
if (answerLabel) {
|
||||
answerLabel.innerHTML = answerLabel.textContent.replace(' *', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (statusSelect && answerField) {
|
||||
toggleAnswerField();
|
||||
statusSelect.addEventListener('change', toggleAnswerField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisierung wenn DOM geladen
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Config wird vom Template bereitgestellt
|
||||
if (window.ticketDetailConfig) {
|
||||
initializeTicketDetail(window.ticketDetailConfig);
|
||||
}
|
||||
});
|
||||
@@ -3,13 +3,18 @@
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<link href="{% static 'css/tailwind.css' %}" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<div class="text-center">
|
||||
<div class="text-6xl mb-4">🔐</div>
|
||||
<h2 class="text-3xl font-bold text-gray-900 mb-2">Anmelden</h2>
|
||||
<p class="text-gray-600 mb-8">Melde dich in deinem Ticketsystem an</p>
|
||||
<div class="mb-4 flex justify-center">
|
||||
<img src="{% static 'IU-logo.png' %}"
|
||||
alt="Logo"
|
||||
class="w-[60%] h-auto" />
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold text-gray-900 mb-2">Studybug</h2>
|
||||
<p class="text-gray-600 mb-8">Melde dich im Korrekturmanagementsystem der IU an</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-md p-8">
|
||||
<form method="post" class="space-y-6">
|
||||
@@ -44,7 +49,7 @@
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
🚀 Anmelden
|
||||
🪲 Anmelden
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
{% extends "ticketsystem/base.html" %}
|
||||
{% block content %}
|
||||
<style>
|
||||
.ticket-list-container {
|
||||
max-width: 700px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background-color: #fafafa;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.ticket-list-container h1 {
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.8rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.ticket-item {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.ticket-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ticket-item a {
|
||||
text-decoration: none;
|
||||
color: #007bff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ticket-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ticket-meta {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<div class="ticket-list-container">
|
||||
<h1>🧾 Meine zugewiesenen Tickets</h1>
|
||||
<p style="color: #777; font-size: 0.9rem; margin-top: -0.5rem; margin-bottom: 1.5rem;">
|
||||
Hinweis: Bereits geschlossene Tickets werden hier nicht aufgelistet.
|
||||
</p>
|
||||
|
||||
{% for ticket in tickets %}
|
||||
<div class="ticket-item">
|
||||
<a href="{% url 'detail' ticket.pk %}">
|
||||
#{{ ticket.id }} – {{ ticket.title }}
|
||||
</a>
|
||||
<div class="ticket-meta">
|
||||
Status: {{ ticket.get_status_display }} |
|
||||
Priorität: {{ ticket.get_priority_display }} |
|
||||
Angelegt am {{ ticket.created_at|date:"d.m.Y H:i" }}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p>Es sind derzeit keine Tickets vorhanden.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -3,35 +3,69 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>
|
||||
{% block title %}TicketSystem{% endblock %}
|
||||
{% block title %}Studybug🪲{% endblock %}
|
||||
</title>
|
||||
<link href="{% static 'css/tailwind.css' %}" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<nav class="bg-gray-700 text-white py-4">
|
||||
<div class="max-w-4xl mx-auto flex justify-between items-center px-4">
|
||||
<div class="flex space-x-4">
|
||||
<a href="{% url 'home' %}" class="text-white hover:text-gray-300">🏠 Start</a>
|
||||
<a href="{% url 'ticket-list' %}" class="text-white hover:text-gray-300">📋 Tickets</a>
|
||||
<a href="{% url 'assigned-tickets' %}"
|
||||
class="text-white hover:text-gray-300">🧾 Meine Tickets</a>
|
||||
<a href="{% url 'faq-list' %}" class="text-white hover:text-gray-300">❓ FAQ</a>
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<!-- Mobile Header -->
|
||||
<div class="flex justify-between items-center md:hidden">
|
||||
<h1 class="text-lg font-semibold">Studybug🪲</h1>
|
||||
<button id="mobile-menu-btn" class="text-2xl">☰</button>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
|
||||
<!-- Desktop Menu -->
|
||||
<div class="hidden md:flex md:justify-between md:items-center">
|
||||
<div class="flex space-x-6">
|
||||
<a href="{% url 'home' %}" class="text-white hover:text-gray-300">🏠 Start</a>
|
||||
<a href="{% url 'ticket-list' %}" class="text-white hover:text-gray-300">📋 Tickets</a>
|
||||
<a href="{% url 'faq-list' %}" class="text-white hover:text-gray-300">❓ FAQ</a>
|
||||
</div>
|
||||
{% if user.is_authenticated %}
|
||||
<span class="text-white">👤 {{ user.username }}</span>
|
||||
<form method="post" action="{% url 'logout' %}" class="inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="text-white hover:text-gray-300">🚪 Logout</button>
|
||||
</form>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span>👤 {{ user.username }}</span>
|
||||
<form method="post" action="{% url 'logout' %}" class="inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="text-white hover:text-gray-300">🚪 Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div id="mobile-menu" class="hidden mt-4">
|
||||
<a href="{% url 'home' %}" class="block text-white py-2">🏠 Start</a>
|
||||
<a href="{% url 'ticket-list' %}" class="block text-white py-2">📋 Tickets</a>
|
||||
<a href="{% url 'faq-list' %}" class="block text-white py-2">❓ FAQ</a>
|
||||
{% if user.is_authenticated %}
|
||||
<div class="border-t border-gray-600 pt-3 mt-3">
|
||||
<span class="block text-white py-1">👤 {{ user.username }}</span>
|
||||
<form method="post" action="{% url 'logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="text-white py-2">🚪 Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Global Container -->
|
||||
<div class="max-w-5xl mx-auto mt-8 px-4">
|
||||
<div class="max-w-6xl mx-auto mt-8 px-4">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Script -->
|
||||
<script>
|
||||
const menuBtn = document.getElementById('mobile-menu-btn');
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
if (menuBtn && menu) {
|
||||
menuBtn.onclick = () => menu.classList.toggle('hidden');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "ticketsystem/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
<!-- Messages -->
|
||||
{% if messages %}
|
||||
@@ -11,6 +12,12 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="max-w-6xl mx-auto px-4 py-6">
|
||||
<div class="mb-6">
|
||||
<a href="{% url 'ticket-list' %}"
|
||||
class="inline-flex items-center text-blue-500 hover:text-blue-600 font-medium">
|
||||
← Zurück zur Übersicht
|
||||
</a>
|
||||
</div>
|
||||
<!-- Ticket Bearbeitung -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<h1 class="text-3xl font-bold mb-4">🎫 Ticket #{{ ticket.id }} – {{ ticket.title }}</h1>
|
||||
@@ -75,7 +82,7 @@
|
||||
{% if form.status.errors %}<div class="text-red-600 text-sm mt-1">{{ form.status.errors }}</div>{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Priorität:</label>
|
||||
<label class="block text-sm font-medium mb-1">Fehlerart:</label>
|
||||
<select name="mistake"
|
||||
{% if not view.can_edit or form.mistake.field.disabled %}disabled{% endif %}
|
||||
class="w-full p-2 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 {% if not view.can_edit or form.mistake.field.disabled %}bg-gray-100{% endif %}">
|
||||
@@ -119,12 +126,12 @@
|
||||
<!-- Bearbeitbares Feld -->
|
||||
<textarea name="answer"
|
||||
rows="4"
|
||||
{% if not view.can_edit or ticket.status != 'resolved' %}disabled{% endif %}
|
||||
{% if not view.can_edit or form.answer.field.disabled %}disabled{% endif %}
|
||||
placeholder="Beschreibe die Lösung des Problems..."
|
||||
class="w-full p-2 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 placeholder:text-black {% if not view.can_edit or ticket.status != 'resolved' %}bg-gray-100 cursor-not-allowed{% endif %}">{{ ticket.answer|default:'' }}</textarea>
|
||||
class="w-full p-2 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 placeholder:text-gray-900 {% if not view.can_edit or ticket.status != 'resolved' or form.answer.field.disabled %}bg-gray-100 cursor-not-allowed{% endif %}">{{ ticket.answer|default:'' }}</textarea>
|
||||
{% if form.answer.errors %}<div class="text-red-600 text-sm mt-1">{{ form.answer.errors }}</div>{% endif %}
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{% if ticket.status != 'resolved' or ticket.status != 'closed' %}
|
||||
{% if ticket.status != 'resolved' %}
|
||||
Eine Antwort ist erforderlich beim Setzen des Status auf "Gelöst"
|
||||
{% endif %}
|
||||
</p>
|
||||
@@ -197,70 +204,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- JavaScript für dynamische Tutor-Anzeige -->
|
||||
<script src="{% static 'js/ticket_detail.js' %}"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const courseSelect = document.getElementById('id_course');
|
||||
const tutorText = document.getElementById('tutor_text');
|
||||
const tutorDisplay = document.getElementById('tutor_display');
|
||||
|
||||
// Course-Tutor Mapping
|
||||
const courseTutorMap = {
|
||||
window.ticketDetailConfig = {
|
||||
canEdit: {% if view.can_edit %}true{% else %}false{% endif %},
|
||||
isCreator: {% if is_creator %}true{% else %}false{% endif %},
|
||||
courseTutorMap: {
|
||||
{% for course in form.course.field.queryset %}
|
||||
{% if course.tutor %}'{{ course.pk }}': '{{ course.tutor.username }}',{% endif %}
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
// Nur aktivieren wenn User bearbeiten kann
|
||||
{% if view.can_edit %}
|
||||
courseSelect.addEventListener('change', function() {
|
||||
const selectedCourseId = this.value;
|
||||
|
||||
if (selectedCourseId && courseTutorMap[selectedCourseId]) {
|
||||
tutorText.textContent = courseTutorMap[selectedCourseId];
|
||||
tutorDisplay.classList.remove('bg-gray-100');
|
||||
tutorDisplay.classList.add('bg-blue-50');
|
||||
} else if (selectedCourseId) {
|
||||
tutorText.textContent = 'Kein Tutor zugewiesen';
|
||||
tutorDisplay.classList.remove('bg-blue-50');
|
||||
tutorDisplay.classList.add('bg-gray-100');
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
});
|
||||
|
||||
// Answer Feld nur bei Status "resolved" aktivieren
|
||||
{% if view.can_edit %}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const statusSelect = document.querySelector('select[name="status"]');
|
||||
const answerField = document.querySelector('textarea[name="answer"]');
|
||||
const answerLabel = answerField.previousElementSibling;
|
||||
|
||||
function toggleAnswerField() {
|
||||
if (statusSelect.value === 'resolved' || statusSelect.value === 'closed') {
|
||||
answerField.disabled = false;
|
||||
answerField.classList.remove('bg-gray-100', 'cursor-not-allowed');
|
||||
answerField.classList.add('bg-white');
|
||||
answerField.required = true;
|
||||
// Pflichtfeld-Stern anzeigen
|
||||
if (!answerLabel.querySelector('.text-red-500')) {
|
||||
answerLabel.innerHTML = answerLabel.textContent + ' <span class="text-red-500">*</span>';
|
||||
}
|
||||
} else {
|
||||
answerField.disabled = true;
|
||||
answerField.classList.add('bg-gray-100', 'cursor-not-allowed');
|
||||
answerField.classList.remove('bg-white');
|
||||
answerField.required = false;
|
||||
// Pflichtfeld-Stern entfernen
|
||||
answerLabel.innerHTML = answerLabel.textContent.replace(' *', '');
|
||||
}
|
||||
}
|
||||
|
||||
// Initial prüfen
|
||||
toggleAnswerField();
|
||||
|
||||
// Bei Status-Änderung prüfen
|
||||
statusSelect.addEventListener('change', toggleAnswerField);
|
||||
});
|
||||
{% endif %}
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,24 +1,52 @@
|
||||
{% extends "ticketsystem/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
<div class="max-w-6xl mx-auto px-4 py-6">
|
||||
<!-- Header -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-3xl font-bold">❓ Häufig gestellte Fragen</h1>
|
||||
<a href="{% url 'faq-pdf-download' %}"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">📄 PDF Download</a>
|
||||
<div class="flex gap-2">
|
||||
<button id="toggle-all-btn"
|
||||
onclick="toggleAllFAQs()"
|
||||
class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded">
|
||||
📂 Alle ausklappen
|
||||
</button>
|
||||
<a href="{% url 'faq-pdf-download' %}"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">📄 PDF Download</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAQ Liste -->
|
||||
{% for faq in faqs %}
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-4">
|
||||
<h3 class="font-bold text-lg mb-2">{{ forloop.counter }}. {{ faq.question }}</h3>
|
||||
<div class="text-gray-700">{{ faq.answer|linebreaks }}</div>
|
||||
<div class="bg-white rounded-lg shadow mb-4 overflow-hidden">
|
||||
<!-- FAQ Header - klickbar -->
|
||||
<button class="w-full p-6 text-left hover:bg-gray-50 focus:outline-none focus:bg-gray-50 transition-colors duration-200"
|
||||
onclick="toggleFAQ({{ forloop.counter0 }})">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="font-bold text-lg pr-4">{{ forloop.counter }}. {{ faq.question }}</h3>
|
||||
<svg id="icon-{{ forloop.counter0 }}"
|
||||
class="w-5 h-5 text-gray-500 transform transition-transform duration-200"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- FAQ Content - versteckt standardmäßig -->
|
||||
<div id="content-{{ forloop.counter0 }}"
|
||||
class="hidden px-6 pb-6">
|
||||
<div class="text-gray-700 border-t pt-4">{{ faq.answer|linebreaks }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="bg-white rounded-lg shadow p-8 text-center">
|
||||
<p class="text-gray-500">Noch keine FAQs vorhanden.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- FAQ JavaScript -->
|
||||
<script src="{% static 'js/faq.js' %}"></script>
|
||||
{% endblock %}
|
||||
@@ -3,10 +3,10 @@
|
||||
<div class="max-w-6xl mx-auto px-4 py-12">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-12">
|
||||
<div class="text-6xl mb-4">🎫</div>
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-4">Willkommen im Ticketsystem</h1>
|
||||
<p class="text-lg text-gray-600 mb-2">Verwalte deine Aufgaben und Tickets effizient</p>
|
||||
<p class="text-gray-500">Was möchten Sie tun?</p>
|
||||
<div class="text-6xl mb-4">🪲</div>
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-4">Willkommen bei Studybug!</h1>
|
||||
<p class="text-lg text-gray-600 mb-2">- Das Korrekturmanagementsystem der IU Internationale Hochschule - </p>
|
||||
<p class="text-gray-500">Erstelle ein Ticket, wenn du Probleme oder Fehler erkannt hast</p>
|
||||
</div>
|
||||
<!-- Quick Actions -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
||||
@@ -25,7 +25,7 @@
|
||||
<p class="text-sm text-green-600">Ein neues Ticket erstellen</p>
|
||||
</a>
|
||||
<!-- Offene Tickets -->
|
||||
<a href="{% url 'ticket-list' %}?status=open"
|
||||
<a href="{% url 'ticket-list' %}?status=new"
|
||||
class="bg-blue-50 rounded-lg shadow-md p-6 text-center hover:shadow-lg transition-shadow duration-200 border border-blue-200">
|
||||
<div class="text-3xl mb-3">📂</div>
|
||||
<h3 class="text-lg font-bold text-blue-800 mb-2">Offene Tickets</h3>
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class="text-sm text-gray-600">Tickets insgesamt</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-4 shadow-sm">
|
||||
<div class="text-2xl font-bold text-red-600 mb-1">{{ open_tickets|default:"0" }}</div>
|
||||
<div class="text-2xl font-bold text-red-600 mb-1">{{ new_tickets|default:"0" }}</div>
|
||||
<div class="text-sm text-gray-600">Offene Tickets</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-4 shadow-sm">
|
||||
|
||||
@@ -2,135 +2,174 @@
|
||||
{% block content %}
|
||||
<!-- Messages -->
|
||||
{% if messages %}
|
||||
<div class="max-w-6xl mx-auto px-4 pt-4">
|
||||
<div class="mb-4">
|
||||
{% for message in messages %}
|
||||
<div class="mb-4 p-3 rounded {% if message.tags == 'error' %}bg-red-100 text-red-700{% elif message.tags == 'success' %}bg-green-100 text-green-700{% else %}bg-yellow-100 text-yellow-700{% endif %}">
|
||||
<div class="mb-2 p-3 rounded text-sm {% if message.tags == 'error' %}bg-red-100 text-red-700{% elif message.tags == 'success' %}bg-green-100 text-green-700{% else %}bg-yellow-100 text-yellow-700{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="max-w-6xl mx-auto px-4 py-6">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">🎫 Ticket-Übersicht</h1>
|
||||
<p class="text-gray-600">Verwalte und verfolge alle deine Tickets</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0">
|
||||
<a href="{% url 'create' %}"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded font-medium">
|
||||
➕ Neues Ticket erstellen
|
||||
</a>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">🎫 Ticket-Übersicht</h1>
|
||||
<p class="text-gray-600">Verwalte und verfolge Tickets</p>
|
||||
</div>
|
||||
<!-- Filter und Suche -->
|
||||
<div class="bg-white rounded-lg shadow p-4 mb-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||
<!-- Status Filter -->
|
||||
<div>
|
||||
<form method="get">
|
||||
<label class="block text-sm font-medium mb-1">Status:</label>
|
||||
<select name="status"
|
||||
onchange="this.form.submit()"
|
||||
class="w-full h-10 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Alle Status</option>
|
||||
{% for value, label in status_choices %}
|
||||
<option value="{{ value }}"
|
||||
{% if selected_status == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<!-- Andere Filter beibehalten -->
|
||||
<div class="mt-4 sm:mt-0">
|
||||
<a href="{% url 'create' %}"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded font-medium block text-center sm:inline-block">
|
||||
➕ Neues Ticket erstellen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Filter und Suche -->
|
||||
<div class="bg-white rounded-lg shadow p-4 mb-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-5 gap-4">
|
||||
<!-- Status Filter -->
|
||||
<div>
|
||||
<form method="get">
|
||||
<label class="block text-sm font-medium mb-1">Status:</label>
|
||||
<select name="status"
|
||||
onchange="this.form.submit()"
|
||||
class="w-full h-10 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Alle Status</option>
|
||||
{% for value, label in status_choices %}
|
||||
<option value="{{ value }}"
|
||||
{% if selected_status == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<!-- Andere Filter beibehalten -->
|
||||
{% if request.GET.assigned_to %}
|
||||
<input type="hidden"
|
||||
name="assigned_to"
|
||||
value="{{ request.GET.assigned_to }}">
|
||||
{% endif %}
|
||||
{% if request.GET.course %}<input type="hidden" name="course" value="{{ request.GET.course }}">{% endif %}
|
||||
{% if search_query %}<input type="hidden" name="q" value="{{ search_query }}">{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
<!-- Meine Tickets Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Zuweisung:</label>
|
||||
{% if request.GET.assigned_to == user.id|stringformat:'s' %}
|
||||
<!-- Wenn aktiv: Button zum Deaktivieren -->
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}{% endif %}{% if selected_course %}&course={{ selected_course }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}"
|
||||
class="block w-full h-10 px-3 border border-purple-300 rounded shadow-sm text-center bg-purple-200 hover:bg-purple-300 transition-colors flex items-center justify-center">
|
||||
👤 Meine Tickets ✓
|
||||
</a>
|
||||
{% else %}
|
||||
<!-- Wenn nicht aktiv: Button zum Aktivieren -->
|
||||
<a href="?assigned_to={{ user.id }}{% if selected_status %}&status={{ selected_status }}{% endif %}{% if selected_course %}&course={{ selected_course }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}"
|
||||
class="block w-full h-10 px-3 border border-purple-300 rounded shadow-sm text-center bg-purple-50 hover:bg-purple-100 transition-colors flex items-center justify-center">
|
||||
👤 Meine Tickets
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Kurs Filter -->
|
||||
<div>
|
||||
<form method="get">
|
||||
<label class="block text-sm font-medium mb-1">Kurs:</label>
|
||||
<select name="course"
|
||||
onchange="this.form.submit()"
|
||||
class="w-full h-10 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Alle Kurse</option>
|
||||
{% for course in courses %}
|
||||
<option value="{{ course.id }}"
|
||||
{% if selected_course == course.id|stringformat:'s' %}selected{% endif %}>
|
||||
{{ course.code }} - {{ course.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<!-- Andere Filter beibehalten -->
|
||||
{% if selected_status %}<input type="hidden" name="status" value="{{ selected_status }}">{% endif %}
|
||||
{% if request.GET.assigned_to %}
|
||||
<input type="hidden"
|
||||
name="assigned_to"
|
||||
value="{{ request.GET.assigned_to }}">
|
||||
{% endif %}
|
||||
{% if search_query %}<input type="hidden" name="q" value="{{ search_query }}">{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
<!-- Suche -->
|
||||
<div class="lg:col-span-2">
|
||||
<form method="get">
|
||||
<label class="block text-sm font-medium mb-1">Suche:</label>
|
||||
<div class="flex">
|
||||
<input type="text"
|
||||
name="q"
|
||||
value="{{ search_query }}"
|
||||
placeholder="Titel oder Beschreibung durchsuchen..."
|
||||
class="flex-1 h-10 px-3 border border-gray-300 rounded-l shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
{% if selected_status %}<input type="hidden" name="status" value="{{ selected_status }}">{% endif %}
|
||||
{% if selected_course %}<input type="hidden" name="course" value="{{ selected_course }}">{% endif %}
|
||||
{% if request.GET.assigned_to %}
|
||||
<input type="hidden"
|
||||
name="assigned_to"
|
||||
value="{{ request.GET.assigned_to }}">
|
||||
{% endif %}
|
||||
{% if search_query %}<input type="hidden" name="q" value="{{ search_query }}">{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
<!-- Meine Tickets Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Zuweisung:</label>
|
||||
{% if request.GET.assigned_to == user.id|stringformat:'s' %}
|
||||
<!-- Wenn aktiv: Button zum Deaktivieren -->
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}"
|
||||
class="block w-full h-10 px-3 border border-purple-300 rounded shadow-sm text-center bg-purple-200 hover:bg-purple-300 transition-colors flex items-center justify-center">
|
||||
👤 Meine Tickets ✓
|
||||
</a>
|
||||
{% else %}
|
||||
<!-- Wenn nicht aktiv: Button zum Aktivieren -->
|
||||
<a href="?assigned_to={{ user.id }}{% if selected_status %}&status={{ selected_status }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}"
|
||||
class="block w-full h-10 px-3 border border-purple-300 rounded shadow-sm text-center bg-purple-50 hover:bg-purple-100 transition-colors flex items-center justify-center">
|
||||
👤 Meine Tickets
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white h-10 px-4 rounded-r">🔍</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Aktive Filter Anzeige -->
|
||||
{% if selected_status or request.GET.assigned_to or search_query %}
|
||||
<div class="mt-4 p-3 bg-gray-50 rounded border-l-4 border-blue-400">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="font-medium text-gray-700">Aktive Filter:</span>
|
||||
{% if selected_status %}
|
||||
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-sm rounded">
|
||||
Status:
|
||||
{% for value, label in status_choices %}
|
||||
{% if value == selected_status %}{{ label }}{% endif %}
|
||||
{% endfor %}
|
||||
<a href="?{% if search_query %}q={{ search_query }}{% endif %}{% if request.GET.assigned_to %}&assigned_to={{ request.GET.assigned_to }}{% endif %}"
|
||||
class="ml-1 text-blue-600 hover:text-blue-800">×</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Suche -->
|
||||
<div class="lg:col-span-2">
|
||||
<form method="get">
|
||||
<label class="block text-sm font-medium mb-1">Suche:</label>
|
||||
<div class="flex">
|
||||
<input type="text"
|
||||
name="q"
|
||||
value="{{ search_query }}"
|
||||
placeholder="Titel oder Beschreibung durchsuchen..."
|
||||
class="flex-1 h-10 px-3 border border-gray-300 rounded-l shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
{% if selected_status %}<input type="hidden" name="status" value="{{ selected_status }}">{% endif %}
|
||||
{% if request.GET.assigned_to %}
|
||||
<input type="hidden"
|
||||
name="assigned_to"
|
||||
value="{{ request.GET.assigned_to }}">
|
||||
{% endif %}
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white h-10 px-4 rounded-r">🔍</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if request.GET.assigned_to %}
|
||||
<span class="px-2 py-1 bg-purple-100 text-purple-800 text-sm rounded">
|
||||
Meine Tickets
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}"
|
||||
class="ml-1 text-purple-600 hover:text-purple-800">×</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if search_query %}
|
||||
<span class="px-2 py-1 bg-green-100 text-green-800 text-sm rounded">
|
||||
Suche: "{{ search_query }}"
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}{% endif %}{% if request.GET.assigned_to %}&assigned_to={{ request.GET.assigned_to }}{% endif %}"
|
||||
class="ml-1 text-green-600 hover:text-green-800">×</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if selected_course %}
|
||||
<span class="px-2 py-1 bg-green-100 text-green-800 text-sm rounded">
|
||||
Kurs:
|
||||
{% for course in courses %}
|
||||
{% if course.id|stringformat:'s' == selected_course %}{{ course.code }}{% endif %}
|
||||
{% endfor %}
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}{% if request.GET.assigned_to %}&assigned_to={{ request.GET.assigned_to }}{% endif %}"
|
||||
class="ml-1 text-green-600 hover:text-green-800">×</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<a href="{% url 'ticket-list' %}"
|
||||
class="text-sm text-gray-600 hover:text-gray-800 font-medium">Alle Filter entfernen</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Aktive Filter Anzeige -->
|
||||
{% if selected_status or request.GET.assigned_to or search_query %}
|
||||
<div class="mt-4 p-3 bg-gray-50 rounded border-l-4 border-blue-400">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="font-medium text-gray-700">Aktive Filter:</span>
|
||||
{% if selected_status %}
|
||||
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-sm rounded">
|
||||
Status:
|
||||
{% for value, label in status_choices %}
|
||||
{% if value == selected_status %}{{ label }}{% endif %}
|
||||
{% endfor %}
|
||||
<a href="?{% if search_query %}q={{ search_query }}{% endif %}{% if request.GET.assigned_to %}&assigned_to={{ request.GET.assigned_to }}{% endif %}"
|
||||
class="ml-1 text-blue-600 hover:text-blue-800">×</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if request.GET.assigned_to %}
|
||||
<span class="px-2 py-1 bg-purple-100 text-purple-800 text-sm rounded">
|
||||
Meine Tickets
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}"
|
||||
class="ml-1 text-purple-600 hover:text-purple-800">×</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if search_query %}
|
||||
<span class="px-2 py-1 bg-green-100 text-green-800 text-sm rounded">
|
||||
Suche: "{{ search_query }}"
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}{% endif %}{% if request.GET.assigned_to %}&assigned_to={{ request.GET.assigned_to }}{% endif %}"
|
||||
class="ml-1 text-green-600 hover:text-green-800">×</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<a href="{% url 'ticket-list' %}"
|
||||
class="text-sm text-gray-600 hover:text-gray-800 font-medium">Alle Filter entfernen</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Ticket Tabelle -->
|
||||
{% if tickets %}
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Ticket Tabelle -->
|
||||
{% if tickets %}
|
||||
<!-- Desktop -->
|
||||
<div class="hidden lg:block bg-white rounded-lg shadow overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-blue-600 text-white">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-center text-sm font-bold">#</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-bold">Titel</th>
|
||||
<th class="px-4 py-3 text-center text-sm font-bold">Kurs</th>
|
||||
<th class="px-4 py-3 text-center text-sm font-bold">Status</th>
|
||||
<th class="px-4 py-3 text-center text-sm font-bold">Fehlerart</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-bold">Zugewiesen an</th>
|
||||
@@ -143,15 +182,20 @@
|
||||
<td class="px-4 py-3 text-center text-sm font-bold text-gray-500">#{{ ticket.id }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<a href="{% url 'detail' ticket.pk %}"
|
||||
class="font-bold text-blue-600 hover:text-blue-800 hover:underline">
|
||||
{{ ticket.title }}
|
||||
class="font-bold text-blue-600 hover:text-blue-800 hover:underline block break-words">
|
||||
{{ ticket.title|truncatechars:30 }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center text-sm">
|
||||
<span class="px-2 py-1 bg-white border border-gray-900 rounded-full text-xs font-bold text-gray-900">
|
||||
{{ ticket.course.code }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
{% if ticket.status == 'new' %}
|
||||
<span class="px-2 py-1 rounded-full text-xs font-bold bg-blue-500 text-white">{{ ticket.get_status_display }}</span>
|
||||
{% elif ticket.status == 'in_progress' %}
|
||||
<span class="px-2 py-1 rounded-full text-xs font-bold bg-yellow-400 text-gray-900">
|
||||
<span class="px-2 py-1 rounded-full text-xs font-bold bg-yellow-400 text-gray-900 whitespace-nowrap">
|
||||
{{ ticket.get_status_display }}
|
||||
</span>
|
||||
{% elif ticket.status == 'resolved' %}
|
||||
@@ -163,7 +207,9 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<span class="px-2 py-1 rounded-full text-xs font-bold bg-blue-500 text-white">{{ ticket.get_mistake_display }}</span>
|
||||
<span class="px-2 py-1 rounded-full text-xs font-bold whitespace-nowrap {% if ticket.mistake == 'typo' %}bg-blue-500 {% elif ticket.mistake == 'formatting_issue' %}bg-purple-500 {% elif ticket.mistake == 'missing_content' %}bg-red-500 {% elif ticket.mistake == 'outdated_content' %}bg-orange-500 {% elif ticket.mistake == 'audio_problem' %}bg-green-500 {% elif ticket.mistake == 'video_problem' %}bg-yellow-500 {% elif ticket.mistake == 'other' %}bg-gray-500 {% else %}bg-gray-400 {% endif %} text-white">
|
||||
{{ ticket.get_mistake_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-600">
|
||||
{% if ticket.assigned_to %}
|
||||
@@ -181,41 +227,78 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<div class="mt-6 flex justify-between items-center">
|
||||
<div class="text-sm text-gray-600">Seite {{ page_obj.number }} von {{ page_obj.paginator.num_pages }}</div>
|
||||
<div class="flex gap-2">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page={{ page_obj.previous_page_number }}{% if selected_status %}&status={{ selected_status }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}"
|
||||
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-50">Zurück</a>
|
||||
{% endif %}
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}{% if selected_status %}&status={{ selected_status }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}"
|
||||
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-50">Weiter</a>
|
||||
</div>
|
||||
<!-- Mobile -->
|
||||
<div class="lg:hidden space-y-4">
|
||||
{% for ticket in tickets %}
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="text-sm font-bold text-gray-500">#{{ ticket.id }}</span>
|
||||
<span class="text-xs text-gray-500">{{ ticket.created_at|date:"d.m.Y" }}</span>
|
||||
</div>
|
||||
<a href="{% url 'detail' ticket.pk %}"
|
||||
class="font-bold text-blue-600 hover:text-blue-800 block mb-2">{{ ticket.title|truncatechars:50 }}</a>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-gray-600">Status:</span>
|
||||
{% if ticket.status == 'new' %}
|
||||
<span class="px-2 py-1 rounded-full text-xs font-bold bg-blue-500 text-white">{{ ticket.get_status_display }}</span>
|
||||
{% elif ticket.status == 'in_progress' %}
|
||||
<span class="px-2 py-1 rounded-full text-xs font-bold bg-yellow-400 text-gray-900">{{ ticket.get_status_display }}</span>
|
||||
{% elif ticket.status == 'resolved' %}
|
||||
<span class="px-2 py-1 rounded-full text-xs font-bold bg-green-400 text-white">{{ ticket.get_status_display }}</span>
|
||||
{% elif ticket.status == 'closed' %}
|
||||
<span class="px-2 py-1 rounded-full text-xs font-bold bg-gray-600 text-white">{{ ticket.get_status_display }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-gray-600">Kurs:</span>
|
||||
<span class="text-sm font-medium">{{ ticket.course.code }}</span>
|
||||
</div>
|
||||
{% if ticket.assigned_to %}
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-gray-600">Zugewiesen:</span>
|
||||
<span class="text-sm">{{ ticket.assigned_to.username }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<!-- Keine Tickets -->
|
||||
<div class="text-center py-12 bg-white rounded-lg shadow">
|
||||
<div class="text-gray-400 text-6xl mb-4">📋</div>
|
||||
{% if search_query %}
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Keine Tickets gefunden</h3>
|
||||
<p class="text-gray-500 mb-4">
|
||||
Keine Tickets gefunden für die Suche „<strong>{{ search_query }}</strong>"
|
||||
</p>
|
||||
<a href="?" class="text-blue-600 hover:text-blue-800 font-medium">Alle Tickets anzeigen</a>
|
||||
{% else %}
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Noch keine Tickets vorhanden</h3>
|
||||
<p class="text-gray-500 mb-4">Erstelle dein erstes Ticket um loszulegen.</p>
|
||||
<a href="{% url 'create' %}"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded font-medium">
|
||||
Erstes Ticket erstellen
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<div class="mt-6 flex justify-between items-center">
|
||||
<div class="text-sm text-gray-600">Seite {{ page_obj.number }} von {{ page_obj.paginator.num_pages }}</div>
|
||||
<div class="flex gap-2">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page={{ page_obj.previous_page_number }}{% if selected_status %}&status={{ selected_status }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}{% if selected_course %}&course={{ selected_course }}{% endif %}"
|
||||
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-50">Zurück</a>
|
||||
{% endif %}
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}{% if selected_status %}&status={{ selected_status }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}{% if selected_course %}&course={{ selected_course }}{% endif %}"
|
||||
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-50">Weiter</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Keine Tickets -->
|
||||
<div class="text-center py-12 bg-white rounded-lg shadow">
|
||||
<div class="text-gray-400 text-6xl mb-4">📋</div>
|
||||
{% if search_query %}
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Keine Tickets gefunden</h3>
|
||||
<p class="text-gray-500 mb-4">
|
||||
Keine Tickets gefunden für die Suche „<strong>{{ search_query }}</strong>"
|
||||
</p>
|
||||
<a href="?" class="text-blue-600 hover:text-blue-800 font-medium">Alle Tickets anzeigen</a>
|
||||
{% else %}
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Noch keine Tickets vorhanden</h3>
|
||||
<p class="text-gray-500 mb-4">Erstelle dein erstes Ticket um loszulegen.</p>
|
||||
<a href="{% url 'create' %}"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded font-medium">
|
||||
Erstes Ticket erstellen
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import (
|
||||
TicketListView,
|
||||
TicketCreateView,
|
||||
TicketUpdateView,
|
||||
HomeView,
|
||||
AssignedTicketListView,
|
||||
TicketDetailUpdateView,
|
||||
faq_list,
|
||||
faq_pdf_download
|
||||
)
|
||||
from .views import (HomeView, TicketCreateView,
|
||||
TicketDetailUpdateView, TicketListView, TicketUpdateView,
|
||||
faq_list, faq_pdf_download)
|
||||
|
||||
urlpatterns = [
|
||||
# /ticketsystem/
|
||||
@@ -21,7 +14,6 @@ urlpatterns = [
|
||||
# /ticketsystem/new/
|
||||
path("new/", TicketCreateView.as_view(), name="create"),
|
||||
path("<int:pk>/modify/", TicketUpdateView.as_view(), name="modify"),
|
||||
path("meine-tickets/", AssignedTicketListView.as_view(), name="assigned-tickets"),
|
||||
path('faq/', faq_list, name='faq-list'),
|
||||
path('faq/download/', faq_pdf_download, name='faq-pdf-download'),
|
||||
path("faq/", faq_list, name="faq-list"),
|
||||
path("faq/download/", faq_pdf_download, name="faq-pdf-download"),
|
||||
]
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
from django.views.generic import ListView, TemplateView
|
||||
from django.views.generic.edit import CreateView, UpdateView
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic.detail import DetailView
|
||||
from django.views.generic.edit import FormMixin
|
||||
from .forms import CommentForm, TicketForm
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import redirect, render
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.views.generic import ListView, TemplateView
|
||||
from django.views.generic.edit import CreateView, UpdateView
|
||||
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
||||
from reportlab.lib.enums import TA_LEFT, TA_CENTER
|
||||
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer
|
||||
|
||||
from .models import Ticket, TicketHistory, FAQ
|
||||
from .forms import CommentForm, TicketForm
|
||||
from .models import FAQ, Course, Ticket, TicketHistory
|
||||
|
||||
|
||||
class HomeView(TemplateView):
|
||||
@@ -27,7 +25,7 @@ class HomeView(TemplateView):
|
||||
context.update(
|
||||
{
|
||||
"total_tickets": Ticket.objects.count(),
|
||||
"open_tickets": Ticket.objects.filter(status="open").count(),
|
||||
"new_tickets": Ticket.objects.filter(status="new").count(),
|
||||
"closed_tickets": Ticket.objects.filter(status="closed").count(),
|
||||
"recent_tickets": Ticket.objects.order_by("-updated_at")[:5],
|
||||
}
|
||||
@@ -47,23 +45,27 @@ class TicketListView(ListView):
|
||||
status = self.request.GET.get("status")
|
||||
assigned_to = self.request.GET.get("assigned_to")
|
||||
query = self.request.GET.get("q")
|
||||
course = self.request.GET.get("course")
|
||||
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
if assigned_to:
|
||||
queryset = queryset.filter(assigned_to_id=assigned_to)
|
||||
if course: # NEU
|
||||
queryset = queryset.filter(course_id=course)
|
||||
if query:
|
||||
queryset = queryset.filter(
|
||||
Q(title__icontains=query) | Q(description__icontains=query)
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["selected_status"] = self.request.GET.get("status", "")
|
||||
context["selected_course"] = self.request.GET.get("course", "")
|
||||
context["search_query"] = self.request.GET.get("q", "")
|
||||
context["status_choices"] = Ticket.STATUS_CHOICES
|
||||
context["courses"] = Course.objects.filter(is_active=True)
|
||||
return context
|
||||
|
||||
|
||||
@@ -73,6 +75,12 @@ class TicketDetailUpdateView(UpdateView):
|
||||
template_name = "ticketsystem/detail.html"
|
||||
comment_form_class = CommentForm
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.can_edit = False
|
||||
self.is_creator = False
|
||||
self.is_tutor = False
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("detail", kwargs={"pk": self.object.pk})
|
||||
|
||||
@@ -86,12 +94,14 @@ class TicketDetailUpdateView(UpdateView):
|
||||
is_superuser = user.is_superuser
|
||||
|
||||
# Bearbeitungsrechte abhängig vom Status
|
||||
if self.ticket.status == 'resolved' and is_creator:
|
||||
if is_superuser:
|
||||
self.can_edit = True
|
||||
elif self.ticket.status == 'closed' and not is_superuser:
|
||||
elif self.ticket.status == "resolved" and is_creator:
|
||||
self.can_edit = True
|
||||
elif self.ticket.status == "closed":
|
||||
self.can_edit = False
|
||||
else:
|
||||
self.can_edit = is_assigned_tutor or is_superuser
|
||||
self.can_edit = is_assigned_tutor
|
||||
|
||||
# Zusätzliche Flags für Template
|
||||
self.is_creator = is_creator
|
||||
@@ -102,16 +112,16 @@ class TicketDetailUpdateView(UpdateView):
|
||||
def get_form_kwargs(self):
|
||||
"""Übergibt zusätzliche kwargs ans Form"""
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['user'] = self.request.user
|
||||
kwargs['ticket'] = self.object
|
||||
kwargs["user"] = self.request.user
|
||||
kwargs["ticket"] = self.object
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Füge Berechtigungs-Infos zum Context hinzu
|
||||
context['is_creator'] = self.is_creator
|
||||
context['is_tutor'] = self.is_tutor
|
||||
context['can_edit'] = self.can_edit
|
||||
context["is_creator"] = self.is_creator
|
||||
context["is_tutor"] = self.is_tutor
|
||||
context["can_edit"] = self.can_edit
|
||||
|
||||
# Kommentarformular hinzufügen
|
||||
if "comment_form" not in context:
|
||||
@@ -128,7 +138,15 @@ class TicketDetailUpdateView(UpdateView):
|
||||
response = super().form_valid(form) # Speichert das Ticket
|
||||
|
||||
# History tracking für geänderte Felder
|
||||
tracked_fields = ["title", "description", "status", "mistake", "course", "answer", "material"]
|
||||
tracked_fields = [
|
||||
"title",
|
||||
"description",
|
||||
"status",
|
||||
"mistake",
|
||||
"course",
|
||||
"answer",
|
||||
"material",
|
||||
]
|
||||
for field in tracked_fields:
|
||||
if field in form.changed_data:
|
||||
old_value = getattr(original, field)
|
||||
@@ -146,11 +164,15 @@ class TicketDetailUpdateView(UpdateView):
|
||||
new_value = str(new_value)
|
||||
elif field == "answer":
|
||||
if old_value:
|
||||
old_value = old_value[:50] + "..." if len(old_value) > 50 else old_value
|
||||
old_value = (
|
||||
old_value[:50] + "..." if len(old_value) > 50 else old_value
|
||||
)
|
||||
else:
|
||||
old_value = "Keine Antwort"
|
||||
if new_value:
|
||||
new_value = new_value[:50] + "..." if len(new_value) > 50 else new_value
|
||||
new_value = (
|
||||
new_value[:50] + "..." if len(new_value) > 50 else new_value
|
||||
)
|
||||
else:
|
||||
new_value = "Keine Antwort"
|
||||
|
||||
@@ -211,18 +233,6 @@ class TicketDetailUpdateView(UpdateView):
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
|
||||
class AssignedTicketListView(LoginRequiredMixin, ListView):
|
||||
model = Ticket
|
||||
template_name = "ticketsystem/assigned_tickets.html"
|
||||
context_object_name = "tickets"
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def get_queryset(self):
|
||||
return Ticket.objects.filter(assigned_to=self.request.user).exclude(
|
||||
status="closed"
|
||||
) # oder "geschlossen", je nach Wahl
|
||||
|
||||
|
||||
class TicketCreateView(CreateView):
|
||||
model = Ticket
|
||||
form_class = TicketForm
|
||||
@@ -280,41 +290,54 @@ class TicketUpdateView(LoginRequiredMixin, UpdateView):
|
||||
def faq_list(request):
|
||||
"""Zeigt alle aktiven FAQs an"""
|
||||
faqs = FAQ.objects.filter(is_active=True)
|
||||
return render(request, 'ticketsystem/faq.html', {'faqs': faqs})
|
||||
return render(request, "ticketsystem/faq.html", {"faqs": faqs})
|
||||
|
||||
|
||||
def faq_pdf_download(request):
|
||||
"""Generiert PDF mit allen FAQs"""
|
||||
# Response Setup
|
||||
response = HttpResponse(content_type='application/pdf')
|
||||
response['Content-Disposition'] = 'attachment; filename="FAQ_Ticketsystem.pdf"'
|
||||
|
||||
current_date = timezone.now()
|
||||
date_string = current_date.strftime("%Y-%m-%d")
|
||||
date_display = current_date.strftime("%d.%m.%Y")
|
||||
|
||||
# Response Setup mit Datum im Dateinamen
|
||||
response = HttpResponse(content_type="application/pdf")
|
||||
response["Content-Disposition"] = (
|
||||
f'attachment; filename="FAQ_Ticketsystem_{date_string}.pdf"'
|
||||
)
|
||||
|
||||
# PDF erstellen
|
||||
doc = SimpleDocTemplate(response, pagesize=A4, topMargin=2 * cm, bottomMargin=2 * cm)
|
||||
doc = SimpleDocTemplate(
|
||||
response, pagesize=A4, topMargin=2 * cm, bottomMargin=2 * cm
|
||||
)
|
||||
|
||||
# Styles
|
||||
styles = getSampleStyleSheet()
|
||||
title_style = ParagraphStyle(
|
||||
'CustomTitle',
|
||||
parent=styles['Heading1'],
|
||||
"CustomTitle",
|
||||
parent=styles["Heading1"],
|
||||
fontSize=24,
|
||||
textColor='#1a1a1a',
|
||||
textColor="#1a1a1a",
|
||||
spaceAfter=20,
|
||||
alignment=TA_CENTER,
|
||||
)
|
||||
subtitle_style = ParagraphStyle(
|
||||
"Subtitle",
|
||||
parent=styles["Normal"],
|
||||
fontSize=12,
|
||||
textColor="#666666",
|
||||
spaceAfter=30,
|
||||
alignment=TA_CENTER
|
||||
alignment=TA_CENTER,
|
||||
)
|
||||
question_style = ParagraphStyle(
|
||||
'Question',
|
||||
parent=styles['Heading2'],
|
||||
"Question",
|
||||
parent=styles["Heading2"],
|
||||
fontSize=14,
|
||||
textColor='#2563eb',
|
||||
spaceAfter=10
|
||||
textColor="#2563eb",
|
||||
spaceAfter=10,
|
||||
)
|
||||
answer_style = ParagraphStyle(
|
||||
'Answer',
|
||||
parent=styles['Normal'],
|
||||
fontSize=11,
|
||||
spaceAfter=20,
|
||||
alignment=TA_LEFT
|
||||
"Answer", parent=styles["Normal"], fontSize=11, spaceAfter=20, alignment=TA_LEFT
|
||||
)
|
||||
|
||||
# Content
|
||||
@@ -322,21 +345,27 @@ def faq_pdf_download(request):
|
||||
|
||||
# Titel
|
||||
elements.append(Paragraph("Häufig gestellte Fragen (FAQ)", title_style))
|
||||
|
||||
elements.append(Paragraph(f"Stand: {date_display}", subtitle_style))
|
||||
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
# FAQs holen
|
||||
faqs = FAQ.objects.filter(is_active=True)
|
||||
|
||||
# FAQs hinzufügen
|
||||
for i, faq in enumerate(faqs, 1):
|
||||
# Frage
|
||||
elements.append(Paragraph(f"{i}. {faq.question}", question_style))
|
||||
if faqs.exists():
|
||||
# FAQs hinzufügen
|
||||
for i, faq in enumerate(faqs, 1):
|
||||
# Frage
|
||||
elements.append(Paragraph(f"{i}. {faq.question}", question_style))
|
||||
|
||||
# Antwort
|
||||
elements.append(Paragraph(faq.answer.replace('\n', '<br/>'), answer_style))
|
||||
# Antwort
|
||||
elements.append(Paragraph(faq.answer.replace("\n", "<br/>"), answer_style))
|
||||
|
||||
# Abstand zwischen FAQs
|
||||
elements.append(Spacer(1, 10))
|
||||
# Abstand zwischen FAQs
|
||||
elements.append(Spacer(1, 10))
|
||||
else:
|
||||
elements.append(Paragraph("Derzeit sind keine FAQs verfügbar.", answer_style))
|
||||
|
||||
# PDF generieren
|
||||
doc.build(elements)
|
||||
|
||||
Reference in New Issue
Block a user