feat: toggle faq accordion

This commit is contained in:
2025-06-11 21:56:44 +02:00
parent 61a7ca6403
commit 8af0bbdb37
4 changed files with 149 additions and 56 deletions

View File

@@ -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

View 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';
}
}

View File

@@ -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>
<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' %}" <a href="{% url 'faq-pdf-download' %}"
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">📄 PDF Download</a> 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>
<!-- FAQ JavaScript -->
<script src="{% static 'js/faq.js' %}"></script>
{% endblock %} {% endblock %}

View File

@@ -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,21 +351,27 @@ 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)
if faqs.exists():
# FAQs hinzufügen # FAQs hinzufügen
for i, faq in enumerate(faqs, 1): for i, faq in enumerate(faqs, 1):
# Frage # Frage
elements.append(Paragraph(f"{i}. {faq.question}", question_style)) 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)