--- id: WP-0014 title: Aufgaben-Phasenzuordnung und Fertigstellungs-Scores in Übersichten status: done phase: 14-of-n created: "2026-05-14" depends_on: WP-0013 --- # WP-0014 — Aufgaben-Phasenzuordnung und Scores Aufgaben bekommen ein Phasenfeld (1–8). In den Übersichten von Ausschreibungen, Losen und der Phasennavigation wird jeweils die Anzahl der verknüpften Aufgaben sowie ein Fertigstellungs-Score angezeigt (z.B. "8/10 (80%)"). Status "erledigt" und "verworfen" gelten als abgeschlossen. --- ```task id: WP-0014-T01 title: Aufgabe.phase — Feld + Migration status: done `apps/aufgaben/models.py` — Feld ergänzen: ```python PHASE_CHOICES = [ (1, 'Recherche & Unterlagen'), (2, 'Teilnahmeentscheidung'), (3, 'Detaillierte Durchsicht'), (4, 'Bieterfragen & Klärung'), (5, 'Preismodell'), (6, 'Unterlagen finalisieren'), (7, 'Abgabe'), (8, 'Zuschlag / Nachbetrachtung'), ] phase = models.PositiveSmallIntegerField( choices=PHASE_CHOICES, null=True, blank=True ) ``` `apps/aufgaben/forms.py` — `phase` zu den Feldern hinzufügen, Widget: `forms.Select(attrs={'class': 'form-input'})`. Migration erstellen und ausführen. ``` ```task id: WP-0014-T02 title: aufgaben_score() Helper in core/services.py status: done Wiederverwendbare Hilfsfunktion, die für ein beliebiges Aufgaben-QuerySet den Score berechnet: ```python def aufgaben_score(qs): """ Gibt dict zurück: total, abgeschlossen, score_pct Abgeschlossen = status in ['erledigt', 'verworfen'] """ total = qs.count() abgeschlossen = qs.filter(status__in=['erledigt', 'verworfen']).count() pct = round(abgeschlossen / total * 100) if total else 0 return {'total': total, 'abgeschlossen': abgeschlossen, 'score_pct': pct} ``` Kein Template-Tag — die Views rufen die Funktion auf und übergeben das Ergebnis direkt im Kontext. ``` ```task id: WP-0014-T03 title: Aufgaben-Score in Ausschreibungen-Liste status: done `apps/ausschreibungen/views.py` — in der Liste-View das QuerySet mit `annotate` ergänzen: ```python from django.db.models import Count, Q qs = qs.annotate( aufgaben_total=Count('aufgaben', distinct=True), aufgaben_erledigt=Count( 'aufgaben', filter=Q(aufgaben__status__in=['erledigt', 'verworfen']), distinct=True, ), ) ``` `templates/ausschreibungen/liste_partial.html` — neue Spalte "Aufgaben" nach der Volumen-Spalte: ```html Aufgaben ... {% if a.aufgaben_total %} {{ a.aufgaben_erledigt }}/{{ a.aufgaben_total }} ({% widthratio a.aufgaben_erledigt a.aufgaben_total 100 %} %) {% else %}—{% endif %} ``` ``` ```task id: WP-0014-T04 title: Aufgaben-Score in Ausschreibungs-Detail status: done `apps/ausschreibungen/views.py` — in `ausschreibung_detail` den Score berechnen und übergeben: ```python from vergabe_teilnahme.apps.core.services import aufgaben_score from vergabe_teilnahme.apps.aufgaben.models import Aufgabe ctx['aufgaben_score'] = aufgaben_score( Aufgabe.objects.filter(ausschreibung=a) ) ``` `templates/ausschreibungen/detail.html` — Aufgaben-Score-Badge im Kopfbereich (neben Phase-Status), z.B.: ```html {% with s=aufgaben_score %} {% if s.total %} Aufgaben: {{ s.abgeschlossen }}/{{ s.total }} ({{ s.score_pct }} %) {% endif %} {% endwith %} ``` ``` ```task id: WP-0014-T05 title: Aufgaben-Score in Lose-Liste status: done `apps/lose/views.py` — in `lose_liste` das Lose-QuerySet annotieren: ```python from django.db.models import Count, Q lose = Los.objects.filter(ausschreibung=a).annotate( aufgaben_total=Count('aufgaben', distinct=True), aufgaben_erledigt=Count( 'aufgaben', filter=Q(aufgaben__status__in=['erledigt', 'verworfen']), distinct=True, ), ) ``` `templates/lose/partials/los_row.html` und `templates/lose/liste.html` — Spalte "Aufgaben" mit Score ergänzen. ``` ```task id: WP-0014-T06 title: Aufgaben-Score in Phase-Navigation status: done `apps/core/services.py` — `build_phase_nav` erweitern: für jede Phase die zugehörigen Aufgaben der Ausschreibung zählen und den Score berechnen. Nur sinnvoll wenn Aufgaben vorhanden sind. ```python from vergabe_teilnahme.apps.aufgaben.models import Aufgabe from django.db.models import Count, Q aufgaben_qs = Aufgabe.objects.filter(ausschreibung=ausschreibung) phasen_stats = { num: aufgaben_score(aufgaben_qs.filter(phase=num)) for num, _ in PHASEN } ``` Jeden Phase-Dict um `aufgaben_score`-Werte erweitern. `templates/partials/phase_nav.html` — pro Phaseneintrag einen Fortschrittsbalken oder Score-Text anzeigen wenn `aufgaben_total > 0`: ```html {% if phase.aufgaben_score.total %} {{ phase.aufgaben_score.abgeschlossen }}/{{ phase.aufgaben_score.total }} {% endif %} ``` ``` ```task id: WP-0014-T07 title: Phasen-Filter in Aufgaben-Liste status: done `apps/aufgaben/views.py` — Phasenfilter ergänzen (analog zum bestehenden Status- und Typ-Filter): ```python phase_filter = request.GET.get('phase') if phase_filter: qs = qs.filter(phase=phase_filter) ``` `templates/aufgaben/liste.html` — Phasen-Dropdown im Filterbereich: ```html
``` Kontext: `phase_choices=Aufgabe.PHASE_CHOICES`, `current_phase=phase_filter or ''`. ``` ```task id: WP-0014-T08 title: Tests + Smoke-Check aller geänderten Seiten status: done pytest ausführen — alle 68 bestehenden Tests müssen grün bleiben. Zusätzlich per Django-Test-Client prüfen: - Ausschreibungen-Liste: Status 200, enthält Score-Spalte - Lose-Liste: Status 200 - Aufgaben-Liste mit ?phase=1: Status 200 - Ausschreibungs-Detail: Status 200 - Aufgabe anlegen mit phase=3: wird korrekt gespeichert ``` ```