feat: toggle faq accordion
This commit is contained in:
@@ -8,6 +8,7 @@ Django==5.2
|
|||||||
djlint==1.36.4
|
djlint==1.36.4
|
||||||
EditorConfig==0.17.0
|
EditorConfig==0.17.0
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
|
isort==6.0.1
|
||||||
jsbeautifier==1.15.4
|
jsbeautifier==1.15.4
|
||||||
json5==0.12.0
|
json5==0.12.0
|
||||||
mypy_extensions==1.1.0
|
mypy_extensions==1.1.0
|
||||||
|
|||||||
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,46 @@
|
|||||||
{% extends "ticketsystem/base.html" %}
|
{% extends "ticketsystem/base.html" %}
|
||||||
|
{% load static %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-6xl mx-auto px-4 py-6">
|
<div class="max-w-6xl mx-auto px-4 py-6">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h1 class="text-3xl font-bold">❓ Häufig gestellte Fragen</h1>
|
<h1 class="text-3xl font-bold">❓ Häufig gestellte Fragen</h1>
|
||||||
<a href="{% url 'faq-pdf-download' %}"
|
<div class="flex gap-2">
|
||||||
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">📄 PDF Download</a>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- FAQ Liste -->
|
<!-- FAQ Liste -->
|
||||||
{% for faq in faqs %}
|
{% for faq in faqs %}
|
||||||
<div class="bg-white rounded-lg shadow p-6 mb-4">
|
<div class="bg-white rounded-lg shadow mb-4 overflow-hidden">
|
||||||
<h3 class="font-bold text-lg mb-2">{{ forloop.counter }}. {{ faq.question }}</h3>
|
<!-- FAQ Header - klickbar -->
|
||||||
<div class="text-gray-700">{{ faq.answer|linebreaks }}</div>
|
<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>
|
</div>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div class="bg-white rounded-lg shadow p-8 text-center">
|
<div class="bg-white rounded-lg shadow p-8 text-center">
|
||||||
@@ -21,4 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
|
<!-- FAQ JavaScript -->
|
||||||
|
<script src="{% static 'js/faq.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
@@ -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.contrib import messages
|
||||||
from django.shortcuts import redirect, render
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import HttpResponse
|
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.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.lib.units import cm
|
||||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer
|
||||||
from reportlab.lib.enums import TA_LEFT, TA_CENTER
|
|
||||||
|
|
||||||
from .models import Ticket, TicketHistory, FAQ, Course
|
from .forms import CommentForm, TicketForm
|
||||||
|
from .models import FAQ, Course, Ticket, TicketHistory
|
||||||
|
|
||||||
|
|
||||||
class HomeView(TemplateView):
|
class HomeView(TemplateView):
|
||||||
@@ -92,9 +90,9 @@ class TicketDetailUpdateView(UpdateView):
|
|||||||
# Bearbeitungsrechte abhängig vom Status
|
# Bearbeitungsrechte abhängig vom Status
|
||||||
if is_superuser:
|
if is_superuser:
|
||||||
self.can_edit = True
|
self.can_edit = True
|
||||||
elif self.ticket.status == 'resolved' and is_creator:
|
elif self.ticket.status == "resolved" and is_creator:
|
||||||
self.can_edit = True
|
self.can_edit = True
|
||||||
elif self.ticket.status == 'closed':
|
elif self.ticket.status == "closed":
|
||||||
self.can_edit = False
|
self.can_edit = False
|
||||||
else:
|
else:
|
||||||
self.can_edit = is_assigned_tutor
|
self.can_edit = is_assigned_tutor
|
||||||
@@ -108,16 +106,16 @@ class TicketDetailUpdateView(UpdateView):
|
|||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
"""Übergibt zusätzliche kwargs ans Form"""
|
"""Übergibt zusätzliche kwargs ans Form"""
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
kwargs['user'] = self.request.user
|
kwargs["user"] = self.request.user
|
||||||
kwargs['ticket'] = self.object
|
kwargs["ticket"] = self.object
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
# Füge Berechtigungs-Infos zum Context hinzu
|
# Füge Berechtigungs-Infos zum Context hinzu
|
||||||
context['is_creator'] = self.is_creator
|
context["is_creator"] = self.is_creator
|
||||||
context['is_tutor'] = self.is_tutor
|
context["is_tutor"] = self.is_tutor
|
||||||
context['can_edit'] = self.can_edit
|
context["can_edit"] = self.can_edit
|
||||||
|
|
||||||
# Kommentarformular hinzufügen
|
# Kommentarformular hinzufügen
|
||||||
if "comment_form" not in context:
|
if "comment_form" not in context:
|
||||||
@@ -134,7 +132,15 @@ class TicketDetailUpdateView(UpdateView):
|
|||||||
response = super().form_valid(form) # Speichert das Ticket
|
response = super().form_valid(form) # Speichert das Ticket
|
||||||
|
|
||||||
# History tracking für geänderte Felder
|
# 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:
|
for field in tracked_fields:
|
||||||
if field in form.changed_data:
|
if field in form.changed_data:
|
||||||
old_value = getattr(original, field)
|
old_value = getattr(original, field)
|
||||||
@@ -152,11 +158,15 @@ class TicketDetailUpdateView(UpdateView):
|
|||||||
new_value = str(new_value)
|
new_value = str(new_value)
|
||||||
elif field == "answer":
|
elif field == "answer":
|
||||||
if old_value:
|
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:
|
else:
|
||||||
old_value = "Keine Antwort"
|
old_value = "Keine Antwort"
|
||||||
if new_value:
|
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:
|
else:
|
||||||
new_value = "Keine Antwort"
|
new_value = "Keine Antwort"
|
||||||
|
|
||||||
@@ -286,41 +296,54 @@ class TicketUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
def faq_list(request):
|
def faq_list(request):
|
||||||
"""Zeigt alle aktiven FAQs an"""
|
"""Zeigt alle aktiven FAQs an"""
|
||||||
faqs = FAQ.objects.filter(is_active=True)
|
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):
|
def faq_pdf_download(request):
|
||||||
"""Generiert PDF mit allen FAQs"""
|
"""Generiert PDF mit allen FAQs"""
|
||||||
# Response Setup
|
|
||||||
response = HttpResponse(content_type='application/pdf')
|
current_date = timezone.now()
|
||||||
response['Content-Disposition'] = 'attachment; filename="FAQ_Ticketsystem.pdf"'
|
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
|
# 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
|
||||||
styles = getSampleStyleSheet()
|
styles = getSampleStyleSheet()
|
||||||
title_style = ParagraphStyle(
|
title_style = ParagraphStyle(
|
||||||
'CustomTitle',
|
"CustomTitle",
|
||||||
parent=styles['Heading1'],
|
parent=styles["Heading1"],
|
||||||
fontSize=24,
|
fontSize=24,
|
||||||
textColor='#1a1a1a',
|
textColor="#1a1a1a",
|
||||||
|
spaceAfter=20,
|
||||||
|
alignment=TA_CENTER,
|
||||||
|
)
|
||||||
|
subtitle_style = ParagraphStyle(
|
||||||
|
"Subtitle",
|
||||||
|
parent=styles["Normal"],
|
||||||
|
fontSize=12,
|
||||||
|
textColor="#666666",
|
||||||
spaceAfter=30,
|
spaceAfter=30,
|
||||||
alignment=TA_CENTER
|
alignment=TA_CENTER,
|
||||||
)
|
)
|
||||||
question_style = ParagraphStyle(
|
question_style = ParagraphStyle(
|
||||||
'Question',
|
"Question",
|
||||||
parent=styles['Heading2'],
|
parent=styles["Heading2"],
|
||||||
fontSize=14,
|
fontSize=14,
|
||||||
textColor='#2563eb',
|
textColor="#2563eb",
|
||||||
spaceAfter=10
|
spaceAfter=10,
|
||||||
)
|
)
|
||||||
answer_style = ParagraphStyle(
|
answer_style = ParagraphStyle(
|
||||||
'Answer',
|
"Answer", parent=styles["Normal"], fontSize=11, spaceAfter=20, alignment=TA_LEFT
|
||||||
parent=styles['Normal'],
|
|
||||||
fontSize=11,
|
|
||||||
spaceAfter=20,
|
|
||||||
alignment=TA_LEFT
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Content
|
# Content
|
||||||
@@ -328,23 +351,29 @@ def faq_pdf_download(request):
|
|||||||
|
|
||||||
# Titel
|
# Titel
|
||||||
elements.append(Paragraph("Häufig gestellte Fragen (FAQ)", title_style))
|
elements.append(Paragraph("Häufig gestellte Fragen (FAQ)", title_style))
|
||||||
|
|
||||||
|
elements.append(Paragraph(f"Stand: {date_display}", subtitle_style))
|
||||||
|
|
||||||
elements.append(Spacer(1, 20))
|
elements.append(Spacer(1, 20))
|
||||||
|
|
||||||
# FAQs holen
|
# FAQs holen
|
||||||
faqs = FAQ.objects.filter(is_active=True)
|
faqs = FAQ.objects.filter(is_active=True)
|
||||||
|
|
||||||
# FAQs hinzufügen
|
if faqs.exists():
|
||||||
for i, faq in enumerate(faqs, 1):
|
# FAQs hinzufügen
|
||||||
# Frage
|
for i, faq in enumerate(faqs, 1):
|
||||||
elements.append(Paragraph(f"{i}. {faq.question}", question_style))
|
# Frage
|
||||||
|
elements.append(Paragraph(f"{i}. {faq.question}", question_style))
|
||||||
|
|
||||||
# Antwort
|
# Antwort
|
||||||
elements.append(Paragraph(faq.answer.replace('\n', '<br/>'), answer_style))
|
elements.append(Paragraph(faq.answer.replace("\n", "<br/>"), answer_style))
|
||||||
|
|
||||||
# Abstand zwischen FAQs
|
# Abstand zwischen FAQs
|
||||||
elements.append(Spacer(1, 10))
|
elements.append(Spacer(1, 10))
|
||||||
|
else:
|
||||||
|
elements.append(Paragraph("Derzeit sind keine FAQs verfügbar.", answer_style))
|
||||||
|
|
||||||
# PDF generieren
|
# PDF generieren
|
||||||
doc.build(elements)
|
doc.build(elements)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|||||||
Reference in New Issue
Block a user