diff --git a/dokumente/migrations/0010_vorgabentable_alter_person_options_vorgabecomment.py b/dokumente/migrations/0010_vorgabentable_alter_person_options_vorgabecomment.py
new file mode 100644
index 0000000..b1937e4
--- /dev/null
+++ b/dokumente/migrations/0010_vorgabentable_alter_person_options_vorgabecomment.py
@@ -0,0 +1,49 @@
+# Generated by Django 5.2.5 on 2025-11-27 22:02
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dokumente', '0009_alter_vorgabe_options_vorgabe_order'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='VorgabenTable',
+ fields=[
+ ],
+ options={
+ 'verbose_name': 'Vorgabe (Tabellenansicht)',
+ 'verbose_name_plural': 'Vorgaben (Tabellenansicht)',
+ 'proxy': True,
+ 'indexes': [],
+ 'constraints': [],
+ },
+ bases=('dokumente.vorgabe',),
+ ),
+ migrations.AlterModelOptions(
+ name='person',
+ options={'ordering': ['name'], 'verbose_name_plural': 'Personen'},
+ ),
+ migrations.CreateModel(
+ name='VorgabeComment',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('text', models.TextField()),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ('vorgabe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='dokumente.vorgabe')),
+ ],
+ options={
+ 'verbose_name': 'Vorgabe-Kommentar',
+ 'verbose_name_plural': 'Vorgabe-Kommentare',
+ 'ordering': ['-created_at'],
+ },
+ ),
+ ]
diff --git a/dokumente/models.py b/dokumente/models.py
index 8c5f716..df90bff 100644
--- a/dokumente/models.py
+++ b/dokumente/models.py
@@ -1,5 +1,6 @@
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
+from django.contrib.auth.models import User
from abschnitte.models import Textabschnitt
from stichworte.models import Stichwort
from referenzen.models import Referenz
@@ -261,3 +262,19 @@ class Changelog(models.Model):
class Meta:
verbose_name_plural="Changelog"
verbose_name="Changelog-Eintrag"
+
+
+class VorgabeComment(models.Model):
+ vorgabe = models.ForeignKey(Vorgabe, on_delete=models.CASCADE, related_name='comments')
+ user = models.ForeignKey(User, on_delete=models.CASCADE)
+ text = models.TextField()
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ verbose_name = "Vorgabe-Kommentar"
+ verbose_name_plural = "Vorgabe-Kommentare"
+ ordering = ['-created_at']
+
+ def __str__(self):
+ return f"Kommentar von {self.user.username} zu {self.vorgabe.Vorgabennummer()}"
diff --git a/dokumente/templates/standards/incomplete_vorgaben.html b/dokumente/templates/standards/incomplete_vorgaben.html
index e6c9f15..b8425f9 100644
--- a/dokumente/templates/standards/incomplete_vorgaben.html
+++ b/dokumente/templates/standards/incomplete_vorgaben.html
@@ -105,13 +105,13 @@
{% else %}
@@ -119,7 +119,7 @@
diff --git a/dokumente/templates/standards/standard_detail.html b/dokumente/templates/standards/standard_detail.html
index f847591..033f96f 100644
--- a/dokumente/templates/standards/standard_detail.html
+++ b/dokumente/templates/standards/standard_detail.html
@@ -145,6 +145,20 @@
{% endif %}
+
+
+ {% if user.is_authenticated %}
+
+
+
+ {% endif %}
@@ -176,4 +190,180 @@
+
+
+
+
+
+
+
{% endblock %}
diff --git a/dokumente/urls.py b/dokumente/urls.py
index a2db977..23e4fe1 100644
--- a/dokumente/urls.py
+++ b/dokumente/urls.py
@@ -8,6 +8,9 @@ urlpatterns = [
path('/history//', views.standard_detail),
path('/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'),
path('/checkliste/', views.standard_checkliste, name='standard_checkliste'),
- path('/json/', views.standard_json, name='standard_json')
+ path('/json/', views.standard_json, name='standard_json'),
+ path('comments//', views.get_vorgabe_comments, name='get_vorgabe_comments'),
+ path('comments//add/', views.add_vorgabe_comment, name='add_vorgabe_comment'),
+ path('comments/delete//', views.delete_vorgabe_comment, name='delete_vorgabe_comment'),
]
diff --git a/dokumente/views.py b/dokumente/views.py
index 0c6df0a..053973f 100644
--- a/dokumente/views.py
+++ b/dokumente/views.py
@@ -2,8 +2,10 @@ from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required, user_passes_test
from django.http import JsonResponse
from django.core.serializers.json import DjangoJSONEncoder
+from django.views.decorators.http import require_POST
+from django.views.decorators.csrf import csrf_exempt
import json
-from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage
+from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
from abschnitte.utils import render_textabschnitte
from datetime import date
@@ -44,6 +46,15 @@ def standard_detail(request, nummer,check_date=""):
for r in vorgabe.referenzen.all():
referenz_items.append(r.Path())
vorgabe.referenzpfade = referenz_items
+
+ # Add comment count
+ if request.user.is_authenticated:
+ if request.user.is_staff:
+ vorgabe.comment_count = vorgabe.comments.count()
+ else:
+ vorgabe.comment_count = vorgabe.comments.filter(user=request.user).count()
+ else:
+ vorgabe.comment_count = 0
return render(request, 'standards/standard_detail.html', {
'standard': standard,
@@ -237,3 +248,83 @@ def standard_json(request, nummer):
# Return JSON response
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)
+
+
+@login_required
+def get_vorgabe_comments(request, vorgabe_id):
+ """Get comments for a specific Vorgabe"""
+ vorgabe = get_object_or_404(Vorgabe, id=vorgabe_id)
+
+ if request.user.is_staff:
+ # Staff can see all comments
+ comments = vorgabe.comments.all().select_related('user')
+ else:
+ # Regular users can only see their own comments
+ comments = vorgabe.comments.filter(user=request.user).select_related('user')
+
+ comments_data = []
+ for comment in comments:
+ comments_data.append({
+ 'id': comment.id,
+ 'text': comment.text,
+ 'user': comment.user.username,
+ 'created_at': comment.created_at.strftime('%d.%m.%Y %H:%M'),
+ 'updated_at': comment.updated_at.strftime('%d.%m.%Y %H:%M'),
+ 'is_own': comment.user == request.user
+ })
+
+ return JsonResponse({'comments': comments_data})
+
+
+@require_POST
+@login_required
+def add_vorgabe_comment(request, vorgabe_id):
+ """Add a new comment to a Vorgabe"""
+ vorgabe = get_object_or_404(Vorgabe, id=vorgabe_id)
+
+ try:
+ data = json.loads(request.body)
+ text = data.get('text', '').strip()
+
+ if not text:
+ return JsonResponse({'error': 'Kommentar darf nicht leer sein'}, status=400)
+
+ comment = VorgabeComment.objects.create(
+ vorgabe=vorgabe,
+ user=request.user,
+ text=text
+ )
+
+ return JsonResponse({
+ 'success': True,
+ 'comment': {
+ 'id': comment.id,
+ 'text': comment.text,
+ 'user': comment.user.username,
+ 'created_at': comment.created_at.strftime('%d.%m.%Y %H:%M'),
+ 'updated_at': comment.updated_at.strftime('%d.%m.%Y %H:%M'),
+ 'is_own': True
+ }
+ })
+
+ except json.JSONDecodeError:
+ return JsonResponse({'error': 'Ungültige Daten'}, status=400)
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=500)
+
+
+@require_POST
+@login_required
+def delete_vorgabe_comment(request, comment_id):
+ """Delete a comment (only own comments or staff can delete)"""
+ comment = get_object_or_404(VorgabeComment, id=comment_id)
+
+ # Check if user can delete this comment
+ if comment.user != request.user and not request.user.is_staff:
+ return JsonResponse({'error': 'Keine Berechtigung zum Löschen dieses Kommentars'}, status=403)
+
+ try:
+ comment.delete()
+ return JsonResponse({'success': True})
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=500)
diff --git a/referenzen/migrations/0003_alter_referenzerklaerung_options.py b/referenzen/migrations/0003_alter_referenzerklaerung_options.py
new file mode 100644
index 0000000..e214b19
--- /dev/null
+++ b/referenzen/migrations/0003_alter_referenzerklaerung_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.2.5 on 2025-11-27 22:02
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('referenzen', '0002_alter_referenz_table_alter_referenzerklaerung_table'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='referenzerklaerung',
+ options={'verbose_name': 'Erklärung', 'verbose_name_plural': 'Erklärungen'},
+ ),
+ ]
diff --git a/static/custom/css/admin_extras.css b/static/custom/css/admin_extras.css
index 2e15826..29450c5 100644
--- a/static/custom/css/admin_extras.css
+++ b/static/custom/css/admin_extras.css
@@ -11,4 +11,93 @@
margin-bottom: 1em;
border: 1px solid #ccc;
padding: 0;
- }
\ No newline at end of file
+ }
+
+/* Comment System Styles */
+.comment-btn {
+ position: relative;
+}
+
+.comment-btn .comment-count {
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ background-color: #dc3545;
+ color: white;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ font-size: 11px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+}
+
+.comment-item {
+ max-width: 100%;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+
+.comment-item .text-muted {
+ font-size: 0.85em;
+}
+
+#commentModal .modal-body {
+ max-height: 60vh;
+ overflow-y: auto;
+}
+
+#commentsContainer {
+ min-height: 100px;
+}
+
+.delete-comment-btn {
+ opacity: 0.7;
+ transition: opacity 0.2s;
+}
+
+.delete-comment-btn:hover {
+ opacity: 1;
+}
+
+.delete-comment-btn {
+ font-size: 18px;
+ font-weight: bold;
+ line-height: 1;
+ color: #721c24;
+ border: 1px solid #f5c6cb;
+ border-radius: 4px;
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+}
+
+.delete-comment-btn:hover {
+ opacity: 1;
+ background-color: #f8d7da;
+ border-color: #f5c6cb;
+}
+
+/* Icon styling for emoji replacements */
+.emoji-icon {
+ font-size: 1.1em;
+ margin-right: 0.3em;
+ vertical-align: middle;
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .comment-item .d-flex {
+ flex-direction: column;
+ }
+
+ .delete-comment-btn {
+ margin-left: 0 !important;
+ margin-top: 0.5rem;
+ }
+}
\ No newline at end of file
diff --git a/stichworte/migrations/0003_alter_stichworterklaerung_options.py b/stichworte/migrations/0003_alter_stichworterklaerung_options.py
new file mode 100644
index 0000000..948e9f4
--- /dev/null
+++ b/stichworte/migrations/0003_alter_stichworterklaerung_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.2.5 on 2025-11-27 22:02
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stichworte', '0002_stichworterklaerung_order'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='stichworterklaerung',
+ options={'verbose_name': 'Erklärung', 'verbose_name_plural': 'Erklärungen'},
+ ),
+ ]