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

242 lines
6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 (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.
---
```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
<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.:
```html
{% 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`:
```html
{% 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:
```html
<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
```
```