feat(nachbetrachtung): Abgabe-Checkliste, Dokumentation und Nachbetrachtung (WP-0009)

Vollständigkeitsprüfung mit Freigaben-Check, Abgabe dokumentieren mit
Nachweis-Upload, Nachbetrachtung mit Kickoff-Aufgabe (gewonnen) und
Alpine.js-gesteuerter Verlustanalyse (verloren). 5 Tests grün.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-05-11 15:09:38 +02:00
parent bca4eb15d8
commit a1cc317b3b
10 changed files with 583 additions and 12 deletions

View file

@ -0,0 +1,81 @@
{% extends "base.html" %}
{% block title %}Abgabe-Checkliste — {{ ausschreibung.titel }}{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-4">
<h1 class="page-title">Abgabe-Checkliste</h1>
<div class="flex gap-2">
<a href="{% url 'ausschreibungen:nachbetrachtung:abgabe:dokumentieren' ausschreibung.pk %}" class="btn-primary text-xs">Abgabe dokumentieren</a>
<a href="{% url 'ausschreibungen:detail' ausschreibung.pk %}" class="btn-ghost text-xs">← Ausschreibung</a>
</div>
</div>
{% if ausschreibung.abgabe_bis %}
<div class="card mb-4 bg-amber-50 border border-amber-200">
<p class="text-sm font-medium text-amber-800">Abgabe bis: <strong>{{ ausschreibung.abgabe_bis }}</strong></p>
</div>
{% endif %}
<div class="card mb-4">
<div class="flex items-center justify-between mb-3">
<p class="text-sm font-medium text-slate-700">Fortschritt</p>
<p class="text-sm font-bold text-slate-700">{{ erfuellt }} / {{ gesamt }}</p>
</div>
<div class="w-full bg-slate-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full" style="width: {% widthratio erfuellt gesamt 100 %}%"></div>
</div>
</div>
<div class="card mb-4">
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Freigaben & Entscheidungen</p>
<ul class="space-y-2">
{% for label, ok in punkte %}
<li class="flex items-center gap-3">
{% if ok %}
<span class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-100 text-green-700 text-xs font-bold"></span>
<span class="text-sm text-slate-700">{{ label }}</span>
{% else %}
<span class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-red-100 text-red-600 text-xs font-bold"></span>
<span class="text-sm text-slate-400">{{ label }}</span>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
<div class="card mb-4">
<div class="flex items-center justify-between mb-3">
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Dokumente</p>
<p class="text-xs text-slate-500">{{ vollstaendigkeit.dokumente_freigegeben }} / {{ vollstaendigkeit.dokumente_gesamt }} freigegeben</p>
</div>
{% if dokumente %}
<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-3">Bezeichnung</th>
<th class="pb-2 pr-3">Kategorie</th>
<th class="pb-2">Status</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for dok in dokumente %}
<tr>
<td class="py-2 pr-3 text-xs">{{ dok.bezeichnung|default:dok.datei.name }}</td>
<td class="py-2 pr-3 text-xs text-slate-500">{{ dok.get_kategorie_display }}</td>
<td class="py-2 text-xs">
{% if dok.status == 'final_abgegeben' or dok.status == 'freigegeben' %}
<span class="text-green-700 font-medium">{{ dok.get_status_display }}</span>
{% else %}
<span class="text-slate-400">{{ dok.get_status_display }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-sm text-slate-400">Noch keine Dokumente hochgeladen.</p>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block title %}Abgabe dokumentieren — {{ ausschreibung.titel }}{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-4">
<h1 class="page-title">Abgabe dokumentieren</h1>
<a href="{% url 'ausschreibungen:nachbetrachtung:abgabe:checkliste' ausschreibung.pk %}" class="btn-ghost text-xs">← Checkliste</a>
</div>
<div class="max-w-xl">
<form method="post" enctype="multipart/form-data" class="card space-y-4">
{% csrf_token %}
<div>
<label class="form-label">{{ form.abgabe_zeitpunkt.label }}</label>
{{ form.abgabe_zeitpunkt }}
{% if form.abgabe_zeitpunkt.errors %}<p class="text-xs text-red-600 mt-1">{{ form.abgabe_zeitpunkt.errors.0 }}</p>{% endif %}
</div>
<div>
<label class="form-label">{{ form.abgabe_plattform.label }}</label>
{{ form.abgabe_plattform }}
</div>
<div>
<label class="form-label">{{ form.verantwortlicher.label }}</label>
{{ form.verantwortlicher }}
</div>
<div>
<label class="form-label">{{ form.abgabenachweis.label }}</label>
{{ form.abgabenachweis }}
<p class="text-xs text-slate-400 mt-1">Eingangsbestätigung, Screenshot o.ä.</p>
</div>
<div>
<label class="form-label">{{ form.kommentar.label }}</label>
{{ form.kommentar }}
</div>
<div class="flex gap-3 pt-2">
<button type="submit" class="btn-primary">Abgabe bestätigen</button>
<a href="{% url 'ausschreibungen:nachbetrachtung:abgabe:checkliste' ausschreibung.pk %}" class="btn-ghost">Abbrechen</a>
</div>
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}Problem bei Abgabe — {{ ausschreibung.titel }}{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-4">
<h1 class="page-title">Problem bei Abgabe vermerken</h1>
<a href="{% url 'ausschreibungen:nachbetrachtung:abgabe:checkliste' ausschreibung.pk %}" class="btn-ghost text-xs">← Checkliste</a>
</div>
<div class="max-w-md">
<form method="post" class="card space-y-4">
{% csrf_token %}
<div>
<label class="form-label">Beschreibung des Problems</label>
<textarea name="kommentar" rows="4" class="form-input w-full" placeholder="Was ist passiert?"></textarea>
</div>
<div class="flex gap-3">
<button type="submit" class="btn-primary bg-amber-600 hover:bg-amber-700">Problem vermerken</button>
<a href="{% url 'ausschreibungen:nachbetrachtung:abgabe:checkliste' ausschreibung.pk %}" class="btn-ghost">Abbrechen</a>
</div>
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,119 @@
{% extends "base.html" %}
{% block title %}Nachbetrachtung — {{ ausschreibung.titel }}{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-4">
<h1 class="page-title">Nachbetrachtung</h1>
<a href="{% url 'ausschreibungen:detail' ausschreibung.pk %}" class="btn-ghost text-xs">← Ausschreibung</a>
</div>
<form method="post" class="space-y-4">
{% csrf_token %}
<div class="card">
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Ergebnis</p>
<div class="flex flex-wrap gap-4">
{% for value, label in form.ergebnis.field.choices %}
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="ergebnis" value="{{ value }}"
{% if form.ergebnis.value == value %}checked{% endif %}
class="text-blue-600">
<span class="text-sm">{{ label }}</span>
</label>
{% endfor %}
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="card">
<label class="form-label">{{ form.zuschlagsdatum.label }}</label>
{{ form.zuschlagsdatum }}
</div>
<div class="card">
<label class="form-label">{{ form.projektverantwortlicher.label }}</label>
{{ form.projektverantwortlicher }}
</div>
</div>
{% if kickoff_aufgabe %}
<div class="card bg-green-50 border border-green-200">
<p class="text-xs font-medium text-green-700 uppercase tracking-wide mb-1">Übergabe</p>
<p class="text-sm text-green-800">
Kickoff-Aufgabe:
<a href="{% url 'ausschreibungen:aufgaben:detail' ausschreibung.pk kickoff_aufgabe.pk %}"
class="underline">{{ kickoff_aufgabe.titel }}</a>
— {{ kickoff_aufgabe.get_status_display }}
</p>
</div>
{% endif %}
<div class="card">
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Abgegebene Unterlagen</p>
<div class="space-y-3">
<div>
<label class="form-label">{{ form.abgegebene_unterlagen.label }}</label>
{{ form.abgegebene_unterlagen }}
</div>
<div>
<label class="form-label">{{ form.abgegebene_preise.label }}</label>
{{ form.abgegebene_preise }}
</div>
</div>
</div>
<div class="card" x-data="{ gruende: {{ verlustgruende_json }} }">
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Verlustanalyse</p>
<div>
<template x-for="(g, i) in gruende" :key="i">
<div class="flex gap-2 mb-2 items-center">
<input x-model="g.grund" class="form-input flex-1" placeholder="Verlustgrund">
<select x-model="g.kategorie" class="form-input w-36">
<option value="preis">Preis</option>
<option value="referenz">Referenz</option>
<option value="anforderung">Anforderung</option>
<option value="subunternehmer">Subunternehmer</option>
<option value="sonstiges">Sonstiges</option>
</select>
<input type="number" x-model.number="g.verlaesslichkeit" min="1" max="5"
class="form-input w-20" placeholder="1-5" title="Verlässlichkeit 1-5">
<button type="button" @click="gruende.splice(i, 1)" class="btn-ghost text-red-500 text-xs"></button>
</div>
</template>
<button type="button"
@click="gruende.push({grund:'', kategorie:'sonstiges', verlaesslichkeit:3})"
class="btn-secondary text-xs mt-1">+ Verlustgrund</button>
</div>
<input type="hidden" name="verlustgruende" :value="JSON.stringify(gruende)">
<div class="mt-4">
<label class="form-label">{{ form.ausschlaggebende_zuschlagsmerkmale.label }}</label>
{{ form.ausschlaggebende_zuschlagsmerkmale }}
</div>
</div>
<div class="card">
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Lessons Learned</p>
<div class="space-y-3">
<div>
<label class="form-label">{{ form.lessons_learned.label }}</label>
{{ form.lessons_learned }}
</div>
<div>
<label class="form-label">{{ form.empfehlungen.label }}</label>
{{ form.empfehlungen }}
</div>
<div class="flex items-center gap-2">
{{ form.wiederverwendbare_erkenntnisse_markiert }}
<label class="text-sm text-slate-700">Wiederverwendbare Erkenntnisse markieren</label>
</div>
</div>
</div>
<div class="flex gap-3">
<button type="submit" class="btn-primary">Speichern</button>
<a href="{% url 'ausschreibungen:detail' ausschreibung.pk %}" class="btn-ghost">Abbrechen</a>
</div>
</form>
{% endblock %}