Files
vergabe-teilnahme/workplans/WP-0014-aufgaben-lose-scores.md
tegwick e4eb5bc368 feat(WP-0014): Aufgaben-Phasenzuordnung und Fertigstellungs-Scores
Aufgabe bekommt ein phase-Feld (1–8). aufgaben_score()-Helper in
core/services.py berechnet abgeschlossen/total/score_pct für jedes
QuerySet. Score-Spalten in Ausschreibungen-Liste, Lose-Liste und
Ausschreibungs-Detail; per-Phase-Scores in der Seitenleisten-Navigation.
Phasenfilter in Aufgaben-Liste. 68 Tests grün.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 01:47:05 +02:00

6.0 KiB
Raw Blame History

id, title, status, phase, created, depends_on
id title status phase created depends_on
WP-0014 Aufgaben-Phasenzuordnung und Fertigstellungs-Scores in Übersichten done 14-of-n 2026-05-14 WP-0013

WP-0014 — Aufgaben-Phasenzuordnung und Scores

Aufgaben bekommen ein Phasenfeld (18). 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.


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.pyphase 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:

<th>Aufgaben</th>
...
<td>
  {% if a.aufgaben_total %}
    {{ a.aufgaben_erledigt }}/{{ a.aufgaben_total }}
    ({% widthratio a.aufgaben_erledigt a.aufgaben_total 100 %} %)
  {% else %}—{% endif %}
</td>

```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.:

{% with s=aufgaben_score %}
{% if s.total %}
<span class="text-xs text-slate-500">
  Aufgaben: {{ s.abgeschlossen }}/{{ s.total }} ({{ s.score_pct }} %)
</span>
{% 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:

{% if phase.aufgaben_score.total %}
<span class="text-xs text-slate-400">
  {{ phase.aufgaben_score.abgeschlossen }}/{{ phase.aufgaben_score.total }}
</span>
{% 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:

<div>
  <label class="form-label">Phase</label>
  <select name="phase" class="form-input text-xs">
    <option value="">Alle Phasen</option>
    {% for val, label in phase_choices %}
    <option value="{{ val }}"{% if current_phase == val|stringformat:"s" %} selected{% endif %}>
      {{ val }}. {{ label }}
    </option>
    {% endfor %}
  </select>
</div>

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