generated from coulomb/repo-seed
Aufgaben und Bieterfragen
This commit is contained in:
@@ -1,3 +1,13 @@
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = []
|
||||
from . import views
|
||||
|
||||
app_name = 'bieterfragen'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.bieterfragen_liste, name='liste'),
|
||||
path('neu/', views.bieterfrage_neu, name='neu'),
|
||||
path('<int:pk>/', views.bieterfrage_detail, name='detail'),
|
||||
path('<int:pk>/status/', views.bieterfrage_status, name='status'),
|
||||
path('<int:pk>/antwort/', views.bieterfrage_antwort, name='antwort'),
|
||||
]
|
||||
|
||||
63
vergabe_teilnahme/apps/aufgaben/forms.py
Normal file
63
vergabe_teilnahme/apps/aufgaben/forms.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from django import forms
|
||||
|
||||
from .models import Aufgabe, Bieterfrage
|
||||
|
||||
|
||||
class AufgabeForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Aufgabe
|
||||
fields = [
|
||||
'titel', 'beschreibung', 'typ', 'prioritaet', 'frist',
|
||||
'verantwortlicher', 'los', 'anforderung', 'bieterfrage',
|
||||
]
|
||||
widgets = {
|
||||
'titel': forms.TextInput(attrs={'class': 'form-input', 'autofocus': True}),
|
||||
'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'typ': forms.Select(attrs={'class': 'form-input'}),
|
||||
'prioritaet': forms.RadioSelect(),
|
||||
'frist': forms.DateInput(attrs={'class': 'form-input', 'type': 'date'}),
|
||||
'verantwortlicher': forms.Select(attrs={'class': 'form-input'}),
|
||||
'los': forms.Select(attrs={'class': 'form-input'}),
|
||||
'anforderung': forms.Select(attrs={'class': 'form-input'}),
|
||||
'bieterfrage': forms.Select(attrs={'class': 'form-input'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, ausschreibung=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if ausschreibung is not None:
|
||||
from vergabe_teilnahme.apps.lose.models import Anforderung, Los
|
||||
self.fields['los'].queryset = Los.objects.filter(ausschreibung=ausschreibung)
|
||||
self.fields['anforderung'].queryset = Anforderung.objects.filter(ausschreibung=ausschreibung)
|
||||
self.fields['bieterfrage'].queryset = Bieterfrage.objects.filter(ausschreibung=ausschreibung)
|
||||
self.fields['beschreibung'].required = False
|
||||
self.fields['frist'].required = False
|
||||
self.fields['verantwortlicher'].required = False
|
||||
self.fields['los'].required = False
|
||||
self.fields['anforderung'].required = False
|
||||
self.fields['bieterfrage'].required = False
|
||||
|
||||
|
||||
class BieterfragenForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Bieterfrage
|
||||
fields = ['fragentext', 'begruendung', 'prioritaet', 'anforderung', 'dokument', 'verfasser']
|
||||
widgets = {
|
||||
'fragentext': forms.Textarea(attrs={'class': 'form-input', 'rows': 4, 'autofocus': True}),
|
||||
'begruendung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'prioritaet': forms.RadioSelect(),
|
||||
'anforderung': forms.Select(attrs={'class': 'form-input'}),
|
||||
'dokument': forms.Select(attrs={'class': 'form-input'}),
|
||||
'verfasser': forms.Select(attrs={'class': 'form-input'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, ausschreibung=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if ausschreibung is not None:
|
||||
from vergabe_teilnahme.apps.lose.models import Anforderung
|
||||
from vergabe_teilnahme.apps.dokumente.models import Dokument
|
||||
self.fields['anforderung'].queryset = Anforderung.objects.filter(ausschreibung=ausschreibung)
|
||||
self.fields['dokument'].queryset = Dokument.objects.filter(ausschreibung=ausschreibung)
|
||||
self.fields['begruendung'].required = False
|
||||
self.fields['anforderung'].required = False
|
||||
self.fields['dokument'].required = False
|
||||
self.fields['verfasser'].required = False
|
||||
7
vergabe_teilnahme/apps/aufgaben/global_urls.py
Normal file
7
vergabe_teilnahme/apps/aufgaben/global_urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.aufgaben_liste, name='aufgaben_global'),
|
||||
]
|
||||
@@ -1,3 +1,96 @@
|
||||
from django.test import TestCase
|
||||
import factory
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
# Create your tests here.
|
||||
from vergabe_teilnahme.apps.ausschreibungen.tests import AusschreibungFactory
|
||||
from vergabe_teilnahme.apps.lose.tests import AnforderungFactory
|
||||
|
||||
from .models import Aufgabe, Bieterfrage
|
||||
|
||||
|
||||
class AufgabeFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Aufgabe
|
||||
|
||||
ausschreibung = factory.SubFactory(AusschreibungFactory)
|
||||
titel = factory.Sequence(lambda n: f"Aufgabe {n}")
|
||||
typ = 'fachlich'
|
||||
status = 'offen'
|
||||
prioritaet = 2
|
||||
|
||||
|
||||
class BieterfragenFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Bieterfrage
|
||||
|
||||
ausschreibung = factory.SubFactory(AusschreibungFactory)
|
||||
fragentext = factory.Sequence(lambda n: f"Frage {n}: Bitte klären Sie...")
|
||||
status = 'entwurf'
|
||||
prioritaet = 2
|
||||
|
||||
|
||||
# ─── Aufgaben ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_aufgaben_liste_get(client):
|
||||
a = AusschreibungFactory()
|
||||
url = reverse('ausschreibungen:aufgaben:liste', kwargs={'ausschreibung_id': a.pk})
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_aufgabe_neu_post(client):
|
||||
a = AusschreibungFactory()
|
||||
url = reverse('ausschreibungen:aufgaben:neu', kwargs={'ausschreibung_id': a.pk})
|
||||
response = client.post(url, {'titel': 'Neue Aufgabe', 'typ': 'fachlich', 'prioritaet': 2})
|
||||
assert response.status_code == 302
|
||||
assert Aufgabe.objects.filter(ausschreibung=a, titel='Neue Aufgabe').exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_aufgabe_status_htmx(client):
|
||||
aufgabe = AufgabeFactory()
|
||||
url = reverse('ausschreibungen:aufgaben:status',
|
||||
kwargs={'ausschreibung_id': aufgabe.ausschreibung_id, 'pk': aufgabe.pk})
|
||||
response = client.post(url, {'status': 'erledigt'}, HTTP_HX_REQUEST='true')
|
||||
assert response.status_code == 200
|
||||
aufgabe.refresh_from_db()
|
||||
assert aufgabe.status == 'erledigt'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_ueberfaellige_aufgabe_auto_update(client):
|
||||
from datetime import date, timedelta
|
||||
a = AusschreibungFactory()
|
||||
aufgabe = AufgabeFactory(ausschreibung=a, frist=date.today() - timedelta(days=1), status='offen')
|
||||
url = reverse('ausschreibungen:aufgaben:liste', kwargs={'ausschreibung_id': a.pk})
|
||||
client.get(url)
|
||||
aufgabe.refresh_from_db()
|
||||
assert aufgabe.status == 'ueberfaellig'
|
||||
|
||||
|
||||
# ─── Bieterfragen ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_bieterfrage_neu_prefill_anforderung(client):
|
||||
a = AusschreibungFactory()
|
||||
anf = AnforderungFactory(ausschreibung=a)
|
||||
url = reverse('ausschreibungen:bieterfragen:neu', kwargs={'ausschreibung_id': a.pk})
|
||||
response = client.get(url, {'anforderung_id': anf.pk})
|
||||
assert response.status_code == 200
|
||||
assert str(anf.pk).encode() in response.content
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_bieterfrage_antwort_speichern(client):
|
||||
bf = BieterfragenFactory(status='eingereicht')
|
||||
url = reverse('ausschreibungen:bieterfragen:antwort',
|
||||
kwargs={'ausschreibung_id': bf.ausschreibung_id, 'pk': bf.pk})
|
||||
response = client.post(url, {'antwort': 'Die Antwort lautet 42.', 'auswirkung_angebot': ''})
|
||||
assert response.status_code == 302
|
||||
bf.refresh_from_db()
|
||||
assert bf.antwort == 'Die Antwort lautet 42.'
|
||||
assert bf.status == 'beantwortet'
|
||||
|
||||
@@ -1,2 +1,15 @@
|
||||
from django.urls import path
|
||||
urlpatterns = []
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'aufgaben'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.aufgaben_liste, name='liste'),
|
||||
path('neu/', views.aufgabe_neu, name='neu'),
|
||||
path('<int:pk>/', views.aufgabe_detail, name='detail'),
|
||||
path('<int:pk>/bearbeiten/', views.aufgabe_bearbeiten, name='bearbeiten'),
|
||||
path('<int:pk>/loeschen/', views.aufgabe_loeschen, name='loeschen'),
|
||||
path('<int:pk>/status/', views.aufgabe_status, name='status'),
|
||||
path('<int:pk>/ergebnis/', views.aufgabe_ergebnis, name='ergebnis'),
|
||||
]
|
||||
|
||||
@@ -1,3 +1,312 @@
|
||||
from django.shortcuts import render
|
||||
from datetime import date
|
||||
|
||||
# Create your views here.
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||
|
||||
from .models import Aufgabe, Bieterfrage
|
||||
|
||||
AKTIVE_STATUS = [
|
||||
'offen', 'in_bearbeitung', 'wartend_intern', 'wartend_sub', 'wartend_ausschreiber',
|
||||
]
|
||||
|
||||
|
||||
def _is_htmx(request):
|
||||
return request.headers.get('HX-Request') == 'true'
|
||||
|
||||
|
||||
# ─── Aufgaben ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def aufgaben_liste(request, ausschreibung_id=None):
|
||||
from vergabe_teilnahme.apps.accounts.models import Mitarbeiter
|
||||
|
||||
heute = date.today()
|
||||
|
||||
if ausschreibung_id:
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
qs = Aufgabe.objects.filter(ausschreibung=ausschreibung)
|
||||
else:
|
||||
ausschreibung = None
|
||||
qs = Aufgabe.objects.select_related('ausschreibung').all()
|
||||
|
||||
qs.filter(frist__lt=heute, status__in=AKTIVE_STATUS).update(status='ueberfaellig')
|
||||
|
||||
status_filter = request.GET.get('status')
|
||||
if status_filter:
|
||||
qs = qs.filter(status=status_filter)
|
||||
|
||||
typ_filter = request.GET.get('typ')
|
||||
if typ_filter:
|
||||
qs = qs.filter(typ=typ_filter)
|
||||
|
||||
verantwortlicher_filter = request.GET.get('verantwortlicher')
|
||||
if verantwortlicher_filter:
|
||||
qs = qs.filter(verantwortlicher=verantwortlicher_filter)
|
||||
|
||||
if not ausschreibung_id and request.GET.get('nur_meine') == '1' and request.user.is_authenticated:
|
||||
qs = qs.filter(verantwortlicher=request.user)
|
||||
|
||||
qs = qs.select_related('ausschreibung', 'verantwortlicher', 'los').order_by('prioritaet', 'frist')
|
||||
|
||||
if ausschreibung:
|
||||
breadcrumbs = [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Aufgaben', 'url': None},
|
||||
]
|
||||
else:
|
||||
breadcrumbs = [{'label': 'Aufgaben', 'url': None}]
|
||||
|
||||
ctx = {
|
||||
'aufgaben': qs,
|
||||
'ausschreibung': ausschreibung,
|
||||
'ausschreibung_id': ausschreibung_id,
|
||||
'status_choices': Aufgabe.STATUS_CHOICES,
|
||||
'typ_choices': Aufgabe.TYP_CHOICES,
|
||||
'mitarbeiter': Mitarbeiter.objects.all(),
|
||||
'current_status': status_filter or '',
|
||||
'current_typ': typ_filter or '',
|
||||
'current_verantwortlicher': verantwortlicher_filter or '',
|
||||
'breadcrumbs': breadcrumbs,
|
||||
}
|
||||
|
||||
if _is_htmx(request):
|
||||
return render(request, 'aufgaben/liste_partial.html', ctx)
|
||||
return render(request, 'aufgaben/liste.html', ctx)
|
||||
|
||||
|
||||
def aufgabe_neu(request, ausschreibung_id):
|
||||
from .forms import AufgabeForm
|
||||
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
if request.method == 'POST':
|
||||
form = AufgabeForm(request.POST, ausschreibung=ausschreibung)
|
||||
if form.is_valid():
|
||||
aufgabe = form.save(commit=False)
|
||||
aufgabe.ausschreibung = ausschreibung
|
||||
aufgabe.save()
|
||||
if _is_htmx(request):
|
||||
return render(request, 'aufgaben/partials/aufgabe_row.html',
|
||||
{'aufgabe': aufgabe, 'ausschreibung': ausschreibung})
|
||||
return redirect('ausschreibungen:aufgaben:liste', ausschreibung_id=ausschreibung_id)
|
||||
else:
|
||||
form = AufgabeForm(ausschreibung=ausschreibung)
|
||||
|
||||
return render(request, 'aufgaben/form.html', {
|
||||
'form': form,
|
||||
'ausschreibung': ausschreibung,
|
||||
'titel': 'Aufgabe anlegen',
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Aufgaben', 'url': f'/ausschreibungen/{ausschreibung_id}/aufgaben/'},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def aufgabe_bearbeiten(request, ausschreibung_id, pk):
|
||||
from .forms import AufgabeForm
|
||||
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung)
|
||||
form = AufgabeForm(request.POST or None, instance=aufgabe, ausschreibung=ausschreibung)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
form.save()
|
||||
return redirect('ausschreibungen:aufgaben:detail', ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
return render(request, 'aufgaben/form.html', {
|
||||
'form': form,
|
||||
'ausschreibung': ausschreibung,
|
||||
'aufgabe': aufgabe,
|
||||
'titel': 'Aufgabe bearbeiten',
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Aufgaben', 'url': f'/ausschreibungen/{ausschreibung_id}/aufgaben/'},
|
||||
{'label': aufgabe.titel[:50], 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def aufgabe_loeschen(request, ausschreibung_id, pk):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung)
|
||||
if request.method == 'POST':
|
||||
aufgabe.status = 'verworfen'
|
||||
aufgabe.save(update_fields=['status'])
|
||||
return redirect('ausschreibungen:aufgaben:liste', ausschreibung_id=ausschreibung_id)
|
||||
return render(request, 'aufgaben/loeschen_confirm.html', {
|
||||
'aufgabe': aufgabe,
|
||||
'ausschreibung': ausschreibung,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Aufgaben', 'url': f'/ausschreibungen/{ausschreibung_id}/aufgaben/'},
|
||||
{'label': 'Löschen', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def aufgabe_detail(request, ausschreibung_id, pk):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung)
|
||||
return render(request, 'aufgaben/detail.html', {
|
||||
'aufgabe': aufgabe,
|
||||
'ausschreibung': ausschreibung,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Aufgaben', 'url': f'/ausschreibungen/{ausschreibung_id}/aufgaben/'},
|
||||
{'label': aufgabe.titel[:50], 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def aufgabe_status(request, ausschreibung_id, pk):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung)
|
||||
if request.method == 'POST':
|
||||
neuer_status = request.POST.get('status')
|
||||
if neuer_status:
|
||||
aufgabe.status = neuer_status
|
||||
aufgabe.save(update_fields=['status'])
|
||||
return render(request, 'aufgaben/partials/aufgabe_row.html',
|
||||
{'aufgabe': aufgabe, 'ausschreibung': ausschreibung})
|
||||
|
||||
|
||||
def aufgabe_ergebnis(request, ausschreibung_id, pk):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung)
|
||||
if request.method == 'POST':
|
||||
aufgabe.ergebnis = request.POST.get('ergebnis', aufgabe.ergebnis)
|
||||
aufgabe.save(update_fields=['ergebnis'])
|
||||
return render(request, 'aufgaben/partials/aufgabe_row.html',
|
||||
{'aufgabe': aufgabe, 'ausschreibung': ausschreibung})
|
||||
|
||||
|
||||
# ─── Bieterfragen ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def bieterfragen_liste(request, ausschreibung_id):
|
||||
from vergabe_teilnahme.apps.accounts.models import Mitarbeiter
|
||||
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
qs = Bieterfrage.objects.filter(ausschreibung=ausschreibung)
|
||||
|
||||
status_filter = request.GET.get('status')
|
||||
if status_filter:
|
||||
qs = qs.filter(status=status_filter)
|
||||
|
||||
prioritaet_filter = request.GET.get('prioritaet')
|
||||
if prioritaet_filter:
|
||||
qs = qs.filter(prioritaet=prioritaet_filter)
|
||||
|
||||
verfasser_filter = request.GET.get('verfasser')
|
||||
if verfasser_filter:
|
||||
qs = qs.filter(verfasser=verfasser_filter)
|
||||
|
||||
qs = qs.select_related('verfasser', 'anforderung').order_by('prioritaet', 'status')
|
||||
|
||||
return render(request, 'aufgaben/bieterfragen_liste.html', {
|
||||
'bieterfragen': qs,
|
||||
'ausschreibung': ausschreibung,
|
||||
'status_choices': Bieterfrage.STATUS_CHOICES,
|
||||
'prioritaet_choices': Bieterfrage.PRIORITAET_CHOICES,
|
||||
'mitarbeiter': Mitarbeiter.objects.all(),
|
||||
'current_status': status_filter or '',
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Bieterfragen', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def bieterfrage_neu(request, ausschreibung_id):
|
||||
from .forms import BieterfragenForm
|
||||
from vergabe_teilnahme.apps.lose.models import Anforderung
|
||||
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
|
||||
initial = {}
|
||||
anforderung_id = request.GET.get('anforderung_id')
|
||||
if anforderung_id:
|
||||
try:
|
||||
anf = Anforderung.objects.get(pk=anforderung_id, ausschreibung=ausschreibung)
|
||||
initial['anforderung'] = anf
|
||||
except Anforderung.DoesNotExist:
|
||||
pass
|
||||
|
||||
if request.method == 'POST':
|
||||
form = BieterfragenForm(request.POST, ausschreibung=ausschreibung)
|
||||
if form.is_valid():
|
||||
bf = form.save(commit=False)
|
||||
bf.ausschreibung = ausschreibung
|
||||
bf.save()
|
||||
return redirect('ausschreibungen:bieterfragen:detail',
|
||||
ausschreibung_id=ausschreibung_id, pk=bf.pk)
|
||||
else:
|
||||
form = BieterfragenForm(initial=initial, ausschreibung=ausschreibung)
|
||||
|
||||
return render(request, 'aufgaben/bieterfrage_form.html', {
|
||||
'form': form,
|
||||
'ausschreibung': ausschreibung,
|
||||
'titel': 'Bieterfrage anlegen',
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Bieterfragen', 'url': f'/ausschreibungen/{ausschreibung_id}/bieterfragen/'},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def bieterfrage_detail(request, ausschreibung_id, pk):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
bieterfrage = get_object_or_404(Bieterfrage, pk=pk, ausschreibung=ausschreibung)
|
||||
return render(request, 'aufgaben/bieterfrage_detail.html', {
|
||||
'bieterfrage': bieterfrage,
|
||||
'ausschreibung': ausschreibung,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Bieterfragen', 'url': f'/ausschreibungen/{ausschreibung_id}/bieterfragen/'},
|
||||
{'label': str(bieterfrage)[:50], 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def bieterfrage_status(request, ausschreibung_id, pk):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
bieterfrage = get_object_or_404(Bieterfrage, pk=pk, ausschreibung=ausschreibung)
|
||||
if request.method == 'POST':
|
||||
neuer_status = request.POST.get('status')
|
||||
if neuer_status:
|
||||
bieterfrage.status = neuer_status
|
||||
if neuer_status == 'eingereicht' and not bieterfrage.einreichungsdatum:
|
||||
bieterfrage.einreichungsdatum = date.today()
|
||||
if neuer_status == 'eingearbeitet':
|
||||
bieterfrage.eingearbeitet = True
|
||||
bieterfrage.save()
|
||||
return redirect('ausschreibungen:bieterfragen:detail',
|
||||
ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
|
||||
|
||||
def bieterfrage_antwort(request, ausschreibung_id, pk):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
bieterfrage = get_object_or_404(Bieterfrage, pk=pk, ausschreibung=ausschreibung)
|
||||
if request.method == 'POST':
|
||||
bieterfrage.antwort = request.POST.get('antwort', bieterfrage.antwort)
|
||||
bieterfrage.auswirkung_angebot = request.POST.get('auswirkung_angebot', bieterfrage.auswirkung_angebot)
|
||||
if bieterfrage.status == 'eingereicht':
|
||||
bieterfrage.status = 'beantwortet'
|
||||
bieterfrage.save()
|
||||
return redirect('ausschreibungen:bieterfragen:detail',
|
||||
ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
return render(request, 'aufgaben/bieterfrage_detail.html', {
|
||||
'bieterfrage': bieterfrage,
|
||||
'ausschreibung': ausschreibung,
|
||||
'show_antwort_form': True,
|
||||
'breadcrumbs': [],
|
||||
})
|
||||
|
||||
126
vergabe_teilnahme/templates/aufgaben/bieterfrage_detail.html
Normal file
126
vergabe_teilnahme/templates/aufgaben/bieterfrage_detail.html
Normal file
@@ -0,0 +1,126 @@
|
||||
{% extends "base.html" %}
|
||||
{% load vergabe_tags %}
|
||||
{% block title %}Bieterfrage{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="page-title">Bieterfrage</h1>
|
||||
<a href="{% url 'ausschreibungen:bieterfragen:liste' ausschreibung.pk %}" class="btn-ghost text-xs">← Übersicht</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="col-span-2 space-y-4">
|
||||
|
||||
<div class="card space-y-3">
|
||||
<p class="text-sm font-medium text-slate-800 whitespace-pre-wrap">{{ bieterfrage.fragentext }}</p>
|
||||
{% if bieterfrage.begruendung %}
|
||||
<div class="border-t border-slate-100 pt-3">
|
||||
<p class="text-xs text-slate-500 mb-1">Begründung</p>
|
||||
<p class="text-sm text-slate-700 whitespace-pre-wrap">{{ bieterfrage.begruendung }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if bieterfrage.antwort or bieterfrage.status == 'eingereicht' or bieterfrage.status == 'beantwortet' or bieterfrage.status == 'eingearbeitet' or show_antwort_form %}
|
||||
<div class="card space-y-3">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Antwort</p>
|
||||
{% if bieterfrage.antwort %}
|
||||
<p class="text-sm text-slate-800 whitespace-pre-wrap">{{ bieterfrage.antwort }}</p>
|
||||
{% if bieterfrage.auswirkung_angebot %}
|
||||
<div class="border-t border-slate-100 pt-3">
|
||||
<p class="text-xs text-slate-500 mb-1">Auswirkung auf Angebot</p>
|
||||
<p class="text-sm text-slate-700 whitespace-pre-wrap">{{ bieterfrage.auswirkung_angebot }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if bieterfrage.status == 'eingereicht' or bieterfrage.status == 'beantwortet' or show_antwort_form %}
|
||||
<form method="post" action="{% url 'ausschreibungen:bieterfragen:antwort' ausschreibung.pk bieterfrage.pk %}"
|
||||
class="space-y-3 border-t border-slate-100 pt-3">
|
||||
{% csrf_token %}
|
||||
<div>
|
||||
<label class="form-label">Antwort eintragen</label>
|
||||
<textarea name="antwort" rows="4" class="form-input w-full">{{ bieterfrage.antwort }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Auswirkung auf Angebot</label>
|
||||
<textarea name="auswirkung_angebot" rows="2" class="form-input w-full">{{ bieterfrage.auswirkung_angebot }}</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary text-xs">Antwort speichern</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
<div class="card">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Status-Verlauf</p>
|
||||
|
||||
<ol class="relative border-l border-slate-200 ml-2 space-y-4 mb-4">
|
||||
{% for val, label in bieterfrage.STATUS_CHOICES %}
|
||||
<li class="ml-4">
|
||||
<span class="absolute -left-1.5 mt-1 w-3 h-3 rounded-full border-2
|
||||
{% if val == bieterfrage.status %}border-blue-600 bg-blue-600{% else %}border-slate-300 bg-white{% endif %}">
|
||||
</span>
|
||||
<p class="text-sm {% if val == bieterfrage.status %}font-semibold text-blue-700{% else %}text-slate-400{% endif %}">
|
||||
{{ label }}
|
||||
{% if val == 'eingereicht' and bieterfrage.einreichungsdatum %}
|
||||
<span class="text-xs font-normal text-slate-500 ml-1">({{ bieterfrage.einreichungsdatum }})</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
|
||||
{% if bieterfrage.status == 'entwurf' %}
|
||||
<form method="post" action="{% url 'ausschreibungen:bieterfragen:status' ausschreibung.pk bieterfrage.pk %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="status" value="abgestimmt">
|
||||
<button type="submit" class="btn-primary text-xs w-full">Abstimmen →</button>
|
||||
</form>
|
||||
{% elif bieterfrage.status == 'abgestimmt' %}
|
||||
<form method="post" action="{% url 'ausschreibungen:bieterfragen:status' ausschreibung.pk bieterfrage.pk %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="status" value="eingereicht">
|
||||
<button type="submit" class="btn-primary text-xs w-full">Einreichen →</button>
|
||||
</form>
|
||||
{% elif bieterfrage.status == 'eingereicht' %}
|
||||
<form method="post" action="{% url 'ausschreibungen:bieterfragen:status' ausschreibung.pk bieterfrage.pk %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="status" value="beantwortet">
|
||||
<button type="submit" class="btn-secondary text-xs w-full">Als beantwortet markieren</button>
|
||||
</form>
|
||||
{% elif bieterfrage.status == 'beantwortet' %}
|
||||
<form method="post" action="{% url 'ausschreibungen:bieterfragen:status' ausschreibung.pk bieterfrage.pk %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="status" value="eingearbeitet">
|
||||
<button type="submit" class="btn-primary text-xs w-full">Einarbeiten ✓</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card space-y-2">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Details</p>
|
||||
<p class="text-xs text-slate-600">Priorität: <span class="font-medium">{{ bieterfrage.get_prioritaet_display }}</span></p>
|
||||
{% if bieterfrage.verfasser %}
|
||||
<p class="text-xs text-slate-600">Verfasser: {{ bieterfrage.verfasser }}</p>
|
||||
{% endif %}
|
||||
{% if bieterfrage.anforderung %}
|
||||
<div class="border-t border-slate-100 pt-2 mt-2">
|
||||
<p class="text-xs text-slate-500 mb-1">Anforderung</p>
|
||||
<a href="{% url 'ausschreibungen:lose:anforderung_detail' ausschreibung.pk bieterfrage.anforderung.pk %}"
|
||||
class="text-xs text-blue-600 hover:underline">{{ bieterfrage.anforderung.titel|truncatechars:50 }}</a>
|
||||
<div class="mt-2">
|
||||
<a href="{% url 'ausschreibungen:lose:anforderung_detail' ausschreibung.pk bieterfrage.anforderung.pk %}"
|
||||
class="text-xs btn-secondary">Anforderungsstatus aktualisieren →</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
50
vergabe_teilnahme/templates/aufgaben/bieterfrage_form.html
Normal file
50
vergabe_teilnahme/templates/aufgaben/bieterfrage_form.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ titel }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="page-title mb-6">{{ titel }}</h1>
|
||||
<form method="post" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
<div class="card space-y-4">
|
||||
<div>
|
||||
<label class="form-label">Fragentext</label>
|
||||
{{ form.fragentext }}
|
||||
{% if form.fragentext.errors %}<p class="text-xs text-red-600 mt-1">{{ form.fragentext.errors.0 }}</p>{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Begründung / Hintergrund</label>
|
||||
{{ form.begruendung }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Priorität</label>
|
||||
<div class="flex gap-4 mt-1">
|
||||
{% for radio in form.prioritaet %}
|
||||
<label class="flex items-center gap-1 text-sm text-slate-700 cursor-pointer">
|
||||
{{ radio.tag }} {{ radio.choice_label }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card space-y-4">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Verknüpfungen (optional)</p>
|
||||
<div>
|
||||
<label class="form-label">Anforderung</label>
|
||||
{{ form.anforderung }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Dokument</label>
|
||||
{{ form.dokument }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Verfasser</label>
|
||||
{{ form.verfasser }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<a href="{% url 'ausschreibungen:bieterfragen:liste' ausschreibung.pk %}" class="btn-ghost">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
80
vergabe_teilnahme/templates/aufgaben/bieterfragen_liste.html
Normal file
80
vergabe_teilnahme/templates/aufgaben/bieterfragen_liste.html
Normal file
@@ -0,0 +1,80 @@
|
||||
{% extends "base.html" %}
|
||||
{% load vergabe_tags %}
|
||||
{% block title %}Bieterfragen — {{ ausschreibung.titel }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="page-title">Bieterfragen</h1>
|
||||
<a href="{% url 'ausschreibungen:bieterfragen:neu' ausschreibung.pk %}" class="btn-primary text-xs">+ Bieterfrage anlegen</a>
|
||||
</div>
|
||||
|
||||
{% if ausschreibung.bieterfragen_frist %}
|
||||
{% with tage=ausschreibung.bieterfragen_frist|timeuntil %}
|
||||
<div class="{% if tage == '0 minutes' %}bg-red-50 border-red-300 text-red-700{% else %}bg-yellow-50 border-yellow-300 text-yellow-700{% endif %} border rounded-lg p-3 mb-4 text-sm">
|
||||
Bieterfragen bis: <strong>{{ ausschreibung.bieterfragen_frist|date:"d.m.Y" }}</strong>
|
||||
{% if tage != '0 minutes' %}— noch {{ tage }}{% else %}— Frist abgelaufen{% endif %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
<form class="card mb-4 flex flex-wrap gap-3 items-end">
|
||||
<div>
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-input text-xs"
|
||||
hx-get="" hx-target="#bieterfragen-list" hx-push-url="true" hx-trigger="change">
|
||||
<option value="">Alle</option>
|
||||
{% for val, label in status_choices %}
|
||||
<option value="{{ val }}"{% if current_status == val %} selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Priorität</label>
|
||||
<select name="prioritaet" class="form-input text-xs"
|
||||
hx-get="" hx-target="#bieterfragen-list" hx-push-url="true" hx-trigger="change">
|
||||
<option value="">Alle</option>
|
||||
{% for val, label in prioritaet_choices %}
|
||||
<option value="{{ val }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="bieterfragen-list" class="card">
|
||||
{% if bieterfragen %}
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-slate-200 text-left text-xs text-slate-500">
|
||||
<th class="pb-2 pr-4">Frage</th>
|
||||
<th class="pb-2 pr-4">Status</th>
|
||||
<th class="pb-2 pr-4">Priorität</th>
|
||||
<th class="pb-2 pr-4">Eingereicht</th>
|
||||
<th class="pb-2">Verfasser</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for bf in bieterfragen %}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="py-2 pr-4">
|
||||
<a href="{% url 'ausschreibungen:bieterfragen:detail' ausschreibung.pk bf.pk %}"
|
||||
class="font-medium text-slate-800 hover:text-blue-600">
|
||||
{{ bf.fragentext|truncatechars:80 }}
|
||||
</a>
|
||||
{% if bf.anforderung %}
|
||||
<p class="text-xs text-slate-400 mt-0.5">Anforderung: {{ bf.anforderung.titel|truncatechars:40 }}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="py-2 pr-4">{% status_badge bf.status bf.get_status_display %}</td>
|
||||
<td class="py-2 pr-4 text-xs">{{ bf.get_prioritaet_display }}</td>
|
||||
<td class="py-2 pr-4 text-xs text-slate-600">{{ bf.einreichungsdatum|default:"—" }}</td>
|
||||
<td class="py-2 text-xs text-slate-600">{{ bf.verfasser|default:"—" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-sm text-slate-500 text-center py-8">Noch keine Bieterfragen angelegt.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
67
vergabe_teilnahme/templates/aufgaben/detail.html
Normal file
67
vergabe_teilnahme/templates/aufgaben/detail.html
Normal file
@@ -0,0 +1,67 @@
|
||||
{% extends "base.html" %}
|
||||
{% load vergabe_tags %}
|
||||
{% block title %}{{ aufgabe.titel }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="page-title">{{ aufgabe.titel }}</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="{% url 'ausschreibungen:aufgaben:bearbeiten' ausschreibung.pk aufgabe.pk %}" class="btn-secondary text-xs">Bearbeiten</a>
|
||||
<a href="{% url 'ausschreibungen:aufgaben:loeschen' ausschreibung.pk aufgabe.pk %}" class="btn-ghost text-xs text-red-600">Verwerfen</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="col-span-2 space-y-4">
|
||||
<div class="card space-y-3">
|
||||
{% render_field aufgabe "typ" "Typ" %}
|
||||
{% render_field aufgabe "prioritaet" "Priorität" %}
|
||||
{% render_field aufgabe "status" "Status" %}
|
||||
{% render_field aufgabe "frist" "Frist" %}
|
||||
{% render_field aufgabe "verantwortlicher" "Verantwortlich" %}
|
||||
{% if aufgabe.beschreibung %}
|
||||
<div>
|
||||
<p class="text-xs text-slate-500 mb-1">Beschreibung</p>
|
||||
<p class="text-sm text-slate-800 whitespace-pre-wrap">{{ aufgabe.beschreibung }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if aufgabe.status == 'erledigt' or aufgabe.ergebnis %}
|
||||
<div class="card">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Ergebnis</p>
|
||||
<form method="post" action="{% url 'ausschreibungen:aufgaben:ergebnis' ausschreibung.pk aufgabe.pk %}">
|
||||
{% csrf_token %}
|
||||
<textarea name="ergebnis" rows="3" class="form-input w-full">{{ aufgabe.ergebnis }}</textarea>
|
||||
<button type="submit" class="btn-primary text-xs mt-2">Speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
{% if aufgabe.anforderung %}
|
||||
<div class="card">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-2">Anforderung</p>
|
||||
<a href="{% url 'ausschreibungen:lose:anforderung_detail' ausschreibung.pk aufgabe.anforderung.pk %}"
|
||||
class="text-sm text-blue-600 hover:underline">{{ aufgabe.anforderung.titel }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if aufgabe.bieterfrage %}
|
||||
<div class="card">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-2">Bieterfrage</p>
|
||||
<a href="{% url 'ausschreibungen:bieterfragen:detail' ausschreibung.pk aufgabe.bieterfrage.pk %}"
|
||||
class="text-sm text-blue-600 hover:underline">{{ aufgabe.bieterfrage }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if aufgabe.los %}
|
||||
<div class="card">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-2">Los</p>
|
||||
<a href="{% url 'ausschreibungen:lose:detail' ausschreibung.pk aufgabe.los.pk %}"
|
||||
class="text-sm text-blue-600 hover:underline">{{ aufgabe.los.lostitel }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
64
vergabe_teilnahme/templates/aufgaben/form.html
Normal file
64
vergabe_teilnahme/templates/aufgaben/form.html
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ titel }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="page-title mb-6">{{ titel }}</h1>
|
||||
<form method="post" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
<div class="card space-y-4">
|
||||
<div>
|
||||
<label class="form-label">Titel</label>
|
||||
{{ form.titel }}
|
||||
{% if form.titel.errors %}<p class="text-xs text-red-600 mt-1">{{ form.titel.errors.0 }}</p>{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Beschreibung</label>
|
||||
{{ form.beschreibung }}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="form-label">Typ</label>
|
||||
{{ form.typ }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Frist</label>
|
||||
{{ form.frist }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Priorität</label>
|
||||
<div class="flex gap-4 mt-1">
|
||||
{% for radio in form.prioritaet %}
|
||||
<label class="flex items-center gap-1 text-sm text-slate-700 cursor-pointer">
|
||||
{{ radio.tag }} {{ radio.choice_label }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Verantwortlich</label>
|
||||
{{ form.verantwortlicher }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card space-y-4">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Verknüpfungen (optional)</p>
|
||||
<div>
|
||||
<label class="form-label">Los</label>
|
||||
{{ form.los }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Anforderung</label>
|
||||
{{ form.anforderung }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Bieterfrage</label>
|
||||
{{ form.bieterfrage }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<a href="{% url 'ausschreibungen:aufgaben:liste' ausschreibung.pk %}" class="btn-ghost">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
57
vergabe_teilnahme/templates/aufgaben/liste.html
Normal file
57
vergabe_teilnahme/templates/aufgaben/liste.html
Normal file
@@ -0,0 +1,57 @@
|
||||
{% extends "base.html" %}
|
||||
{% load vergabe_tags %}
|
||||
{% block title %}Aufgaben{% if ausschreibung %} — {{ ausschreibung.titel }}{% endif %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="page-title">Aufgaben</h1>
|
||||
{% if ausschreibung %}
|
||||
<a href="{% url 'ausschreibungen:aufgaben:neu' ausschreibung.pk %}" class="btn-primary text-xs">+ Aufgabe anlegen</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form class="card mb-4 flex flex-wrap gap-3 items-end"
|
||||
hx-get="" hx-target="#aufgaben-container" hx-push-url="true"
|
||||
hx-trigger="change from:select, submit">
|
||||
{% csrf_token %}
|
||||
<div>
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-input text-xs">
|
||||
<option value="">Alle</option>
|
||||
{% for val, label in status_choices %}
|
||||
<option value="{{ val }}"{% if current_status == val %} selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Typ</label>
|
||||
<select name="typ" class="form-input text-xs">
|
||||
<option value="">Alle</option>
|
||||
{% for val, label in typ_choices %}
|
||||
<option value="{{ val }}"{% if current_typ == val %} selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Verantwortlich</label>
|
||||
<select name="verantwortlicher" class="form-input text-xs">
|
||||
<option value="">Alle</option>
|
||||
{% for m in mitarbeiter %}
|
||||
<option value="{{ m.pk }}"{% if current_verantwortlicher == m.pk|stringformat:"s" %} selected{% endif %}>{{ m.get_full_name|default:m.username }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if not ausschreibung %}
|
||||
<label class="flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="nur_meine" value="1"
|
||||
{% if request.GET.nur_meine == "1" %}checked{% endif %}>
|
||||
Nur meine
|
||||
</label>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<div id="aufgaben-container" class="card">
|
||||
{% include "aufgaben/liste_partial.html" %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
22
vergabe_teilnahme/templates/aufgaben/liste_partial.html
Normal file
22
vergabe_teilnahme/templates/aufgaben/liste_partial.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% load vergabe_tags %}
|
||||
{% if aufgaben %}
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-slate-200 text-left text-xs text-slate-500">
|
||||
<th class="pb-2 pr-4">Titel</th>
|
||||
<th class="pb-2 pr-4">Typ</th>
|
||||
<th class="pb-2 pr-4">Frist</th>
|
||||
<th class="pb-2 pr-4">Verantwortlich</th>
|
||||
<th class="pb-2 pr-4">Status</th>
|
||||
<th class="pb-2">Ergebnis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="aufgaben-table" class="divide-y divide-slate-100">
|
||||
{% for aufgabe in aufgaben %}
|
||||
{% include "aufgaben/partials/aufgabe_row.html" %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-sm text-slate-500 text-center py-8">Keine Aufgaben gefunden.</p>
|
||||
{% endif %}
|
||||
18
vergabe_teilnahme/templates/aufgaben/loeschen_confirm.html
Normal file
18
vergabe_teilnahme/templates/aufgaben/loeschen_confirm.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Aufgabe verwerfen{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto mt-16">
|
||||
<div class="card space-y-4">
|
||||
<h1 class="text-lg font-semibold text-slate-800">Aufgabe verwerfen?</h1>
|
||||
<p class="text-sm text-slate-600">
|
||||
Die Aufgabe wird nicht gelöscht, sondern auf Status <strong>Verworfen</strong> gesetzt.
|
||||
</p>
|
||||
<p class="text-sm font-medium text-slate-800 bg-slate-50 rounded p-3">{{ aufgabe.titel }}</p>
|
||||
<form method="post" class="flex gap-3">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn-primary bg-red-600 hover:bg-red-700">Verwerfen</button>
|
||||
<a href="{% url 'ausschreibungen:aufgaben:detail' ausschreibung.pk aufgabe.pk %}" class="btn-ghost">Abbrechen</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,45 @@
|
||||
{% load vergabe_tags %}
|
||||
<tr class="hover:bg-slate-50{% if aufgabe.status == 'ueberfaellig' %} bg-red-50{% endif %}">
|
||||
<td class="py-2 pr-4">
|
||||
<a href="{% url 'ausschreibungen:aufgaben:detail' ausschreibung.pk aufgabe.pk %}"
|
||||
class="font-medium text-slate-800 hover:text-blue-600 line-clamp-1">
|
||||
{{ aufgabe.titel|truncatechars:60 }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="py-2 pr-4 text-xs">{% status_badge aufgabe.typ aufgabe.get_typ_display %}</td>
|
||||
<td class="py-2 pr-4 text-xs">
|
||||
{% if aufgabe.frist %}
|
||||
<span class="{% if aufgabe.status == 'ueberfaellig' %}text-red-600 font-medium{% else %}text-slate-600{% endif %}">
|
||||
{{ aufgabe.frist }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-slate-400">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="py-2 pr-4 text-xs text-slate-600">{{ aufgabe.verantwortlicher|default:"—" }}</td>
|
||||
<td class="py-2 pr-4">
|
||||
<select name="status"
|
||||
hx-post="{% url 'ausschreibungen:aufgaben:status' ausschreibung.pk aufgabe.pk %}"
|
||||
hx-target="closest tr"
|
||||
hx-swap="outerHTML"
|
||||
class="form-input text-xs py-1">
|
||||
{% for val, label in aufgabe.STATUS_CHOICES %}
|
||||
<option value="{{ val }}"{% if val == aufgabe.status %} selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
{% if aufgabe.status == 'erledigt' %}
|
||||
<td class="py-2">
|
||||
<form hx-post="{% url 'ausschreibungen:aufgaben:ergebnis' ausschreibung.pk aufgabe.pk %}"
|
||||
hx-target="closest tr" hx-swap="outerHTML"
|
||||
class="flex gap-1 items-center">
|
||||
{% csrf_token %}
|
||||
<input type="text" name="ergebnis" value="{{ aufgabe.ergebnis }}"
|
||||
placeholder="Ergebnis…" class="form-input text-xs py-1 w-40">
|
||||
<button type="submit" class="btn-primary text-xs py-1 px-2">OK</button>
|
||||
</form>
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="py-2 text-xs text-slate-500">{{ aufgabe.ergebnis|truncatechars:30 }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
@@ -25,7 +25,7 @@ urlpatterns = [
|
||||
path('', home, name='home'),
|
||||
path('ausschreibungen/', include('vergabe_teilnahme.apps.ausschreibungen.urls', namespace='ausschreibungen')),
|
||||
path('lose/', include('vergabe_teilnahme.apps.lose.urls')),
|
||||
path('aufgaben/', include('vergabe_teilnahme.apps.aufgaben.urls')),
|
||||
path('aufgaben/', include('vergabe_teilnahme.apps.aufgaben.global_urls')),
|
||||
path('dokumente/', include('vergabe_teilnahme.apps.dokumente.urls')),
|
||||
path('preise/', include('vergabe_teilnahme.apps.preise.urls')),
|
||||
path('partner/', include('vergabe_teilnahme.apps.partner.urls')),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: WP-0006
|
||||
title: Aufgaben und Bieterfragen
|
||||
status: todo
|
||||
status: done
|
||||
phase: 6-of-12
|
||||
created: "2026-05-08"
|
||||
depends_on: WP-0005
|
||||
@@ -17,7 +17,7 @@ Implementiert alle Views für Aufgaben (UC-AU-01 bis UC-AU-04) und Bieterfragen
|
||||
```task
|
||||
id: WP-0006-T01
|
||||
title: Aufgabenliste pro Ausschreibung und globale Liste (UC-OV-03, UC-AU-01)
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
`aufgaben/views.py` — aufgaben_liste:
|
||||
|
||||
@@ -45,7 +45,7 @@ Globale URL in Haupt-urls.py: `path('aufgaben/', include('vergabe_teilnahme.apps
|
||||
```task
|
||||
id: WP-0006-T02
|
||||
title: Aufgabe anlegen und zuweisen (UC-AU-01)
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
`AufgabeForm(ModelForm)`:
|
||||
- `typ` als Select, `prioritaet` als Radio (Hoch/Mittel/Niedrig)
|
||||
@@ -63,7 +63,7 @@ status: todo
|
||||
```task
|
||||
id: WP-0006-T03
|
||||
title: Aufgabenstatus inline ändern und Ergebnis dokumentieren (UC-AU-03)
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
**Status-Widget** (analog zum Ausschreibungs-Status-Widget):
|
||||
Jede Zeile in der Aufgabenliste enthält ein Status-Dropdown:
|
||||
@@ -89,7 +89,7 @@ Nutzer kann Ergebnis eintragen und separat abspeichern.
|
||||
```task
|
||||
id: WP-0006-T04
|
||||
title: Bieterfragen-Liste und Bieterfrage anlegen (UC-BF-01, UC-BF-02)
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
`aufgaben/views.py` — bieterfragen_liste und bieterfrage_neu:
|
||||
|
||||
@@ -118,7 +118,7 @@ path('<int:pk>/antwort/', views.bieterfrage_antwort, name='antwort'),
|
||||
```task
|
||||
id: WP-0006-T05
|
||||
title: Bieterfragen-Workflow und Antwort einarbeiten (UC-BF-03)
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
`bieterfrage_status (POST)`: Ermöglicht Status-Wechsel über definierte Übergänge:
|
||||
entwurf → abgestimmt → eingereicht → beantwortet → eingearbeitet.
|
||||
@@ -138,7 +138,7 @@ Auf der Bieterfragen-Detailseite:
|
||||
```task
|
||||
id: WP-0006-T06
|
||||
title: Aufgaben- und Bieterfragen-Tests
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
`aufgaben/tests/test_views.py`:
|
||||
- Test: Aufgabenliste gibt 200 zurück
|
||||
|
||||
Reference in New Issue
Block a user