feat(WP-0011): Marktbegleiter-Analyse — Katalog, Passagen, Auswertung

Implementiert UC-MB-01 bis UC-MB-03: Marktbegleiter-Katalog (Liste,
Detail, Anlegen/Bearbeiten), Ausschreibungspassagen mit Verlässlichkeitsscore
(1–10, Validator), Musterauswertung mit Aggregationen (Ausschreiber-Häufigkeit,
Ø-Score, Passagen-Anzahl). 4 Tests grün.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-05-11 16:20:55 +02:00
parent f88e2e7562
commit bde10f3a69
13 changed files with 737 additions and 12 deletions

View file

@ -0,0 +1,79 @@
{% extends "base.html" %}
{% block title %}Auswertung — {{ marktbegleiter.name }}{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-5">
<h1 class="page-title">Auswertung: {{ marktbegleiter.name }}</h1>
<a href="{% url 'marktbegleiter:detail' pk=marktbegleiter.pk %}" class="btn-secondary">Zurück</a>
</div>
<div class="grid grid-cols-3 gap-4 mb-6">
<div class="card text-center">
<p class="text-3xl font-bold text-brand-600">{{ passagen.count }}</p>
<p class="text-sm text-slate-500 mt-1">Passagen gesamt</p>
</div>
<div class="card text-center">
<p class="text-3xl font-bold text-brand-600">{{ anzahl_ausschreibungen }}</p>
<p class="text-sm text-slate-500 mt-1">Ausschreibungen</p>
</div>
<div class="card text-center">
<p class="text-3xl font-bold text-brand-600">
{% if score_durchschnitt %}{{ score_durchschnitt|floatformat:1 }}{% else %}—{% endif %}
</p>
<p class="text-sm text-slate-500 mt-1">Ø Verlässlichkeit</p>
</div>
</div>
{% if ausschreiber_haeufigkeit %}
<div class="card mb-5">
<h2 class="section-title mb-4">Häufigste Ausschreiber</h2>
<table class="w-full text-sm">
<thead>
<tr class="text-left border-b border-slate-200">
<th class="pb-2 font-medium text-slate-600">Ausschreiber</th>
<th class="pb-2 font-medium text-slate-600">Passagen</th>
</tr>
</thead>
<tbody>
{% for row in ausschreiber_haeufigkeit %}
<tr class="border-b border-slate-100">
<td class="py-2 text-slate-700">{{ row.ausschreibung__ausschreiber|default:"(unbekannt)" }}</td>
<td class="py-2 font-medium">{{ row.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<div class="card">
<h2 class="section-title mb-4">Alle Passagen</h2>
{% if passagen %}
<table class="w-full text-sm">
<thead>
<tr class="text-left border-b border-slate-200">
<th class="pb-2 font-medium text-slate-600">Ausschreibung</th>
<th class="pb-2 font-medium text-slate-600">Textauszug</th>
<th class="pb-2 font-medium text-slate-600">Score</th>
<th class="pb-2 font-medium text-slate-600">Kategorie</th>
<th class="pb-2 font-medium text-slate-600">Datum</th>
</tr>
</thead>
<tbody>
{% for p in passagen %}
<tr class="border-b border-slate-100 hover:bg-slate-50">
<td class="py-2 text-slate-700">{{ p.ausschreibung.titel|truncatechars:40 }}</td>
<td class="py-2 text-slate-600 max-w-xs">{{ p.passage|truncatechars:150 }}</td>
<td class="py-2 font-medium {% if p.verlaesslichkeitsscore >= 7 %}text-green-600{% elif p.verlaesslichkeitsscore >= 4 %}text-yellow-600{% else %}text-red-500{% endif %}">
{{ p.verlaesslichkeitsscore }}
</td>
<td class="py-2 text-slate-600">{{ p.kategorie|default:"—" }}</td>
<td class="py-2 text-slate-500 text-xs">{{ p.erfassungsdatum }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-slate-500 text-sm">Keine Passagen vorhanden.</p>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,118 @@
{% extends "base.html" %}
{% block title %}{{ obj.name }}{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-5">
<h1 class="page-title">{{ obj.name }}</h1>
<div class="flex gap-2">
<a href="{% url 'marktbegleiter:auswertung' pk=obj.pk %}" class="btn-secondary">Auswertung</a>
<a href="{% url 'marktbegleiter:bearbeiten' pk=obj.pk %}" class="btn-primary">Bearbeiten</a>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5 mb-5">
<div class="card col-span-2 space-y-4">
{% if obj.kurzbeschreibung %}
<div>
<h2 class="section-title">Beschreibung</h2>
<p class="text-sm text-slate-700">{{ obj.kurzbeschreibung }}</p>
</div>
{% endif %}
{% if obj.produkt_leistungsportfolio %}
<div>
<h2 class="section-title">Portfolio</h2>
<p class="text-sm text-slate-700 whitespace-pre-line">{{ obj.produkt_leistungsportfolio }}</p>
</div>
{% endif %}
{% if obj.bekannte_staerken %}
<div>
<h2 class="section-title">Stärken</h2>
<p class="text-sm text-slate-700 whitespace-pre-line">{{ obj.bekannte_staerken }}</p>
</div>
{% endif %}
{% if obj.bekannte_schwaechen %}
<div>
<h2 class="section-title">Schwächen</h2>
<p class="text-sm text-slate-700 whitespace-pre-line">{{ obj.bekannte_schwaechen }}</p>
</div>
{% endif %}
{% if obj.typische_formulierungen %}
<div>
<h2 class="section-title">Typische Formulierungen</h2>
<div class="flex flex-wrap gap-2">
{% for zeile in obj.typische_formulierungen.splitlines %}
{% if zeile.strip %}
<span class="px-2 py-1 bg-slate-100 text-slate-700 rounded text-xs">{{ zeile.strip }}</span>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
</div>
<div class="space-y-4">
<div class="card">
<h2 class="section-title mb-3">Kennzahlen</h2>
<dl class="space-y-2 text-sm">
<div class="flex justify-between">
<dt class="text-slate-500">Ausschreibungen</dt>
<dd class="font-medium">{{ anzahl_ausschreibungen }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-slate-500">Passagen gesamt</dt>
<dd class="font-medium">{{ passagen.count }}</dd>
</div>
{% if score_durchschnitt %}
<div class="flex justify-between">
<dt class="text-slate-500">Ø Verlässlichkeit</dt>
<dd class="font-medium">{{ score_durchschnitt|floatformat:1 }}</dd>
</div>
{% endif %}
<div class="flex justify-between">
<dt class="text-slate-500">Vertraulichkeit</dt>
<dd>{{ obj.get_vertraulichkeit_display }}</dd>
</div>
{% if obj.letzte_aktualisierung %}
<div class="flex justify-between">
<dt class="text-slate-500">Aktualisiert</dt>
<dd>{{ obj.letzte_aktualisierung }}</dd>
</div>
{% endif %}
</dl>
</div>
</div>
</div>
<div class="card">
<h2 class="section-title mb-4">Verknüpfte Passagen</h2>
{% if passagen %}
<table class="w-full text-sm">
<thead>
<tr class="text-left border-b border-slate-200">
<th class="pb-2 font-medium text-slate-600">Ausschreibung</th>
<th class="pb-2 font-medium text-slate-600">Textauszug</th>
<th class="pb-2 font-medium text-slate-600">Score</th>
<th class="pb-2 font-medium text-slate-600">Kategorie</th>
<th class="pb-2 font-medium text-slate-600">Datum</th>
</tr>
</thead>
<tbody>
{% for p in passagen %}
<tr class="border-b border-slate-100 hover:bg-slate-50">
<td class="py-2 text-slate-700">{{ p.ausschreibung.titel|truncatechars:40 }}</td>
<td class="py-2 text-slate-600 max-w-xs">{{ p.passage|truncatechars:150 }}</td>
<td class="py-2">
<span class="font-medium {% if p.verlaesslichkeitsscore >= 7 %}text-green-600{% elif p.verlaesslichkeitsscore >= 4 %}text-yellow-600{% else %}text-red-500{% endif %}">
{{ p.verlaesslichkeitsscore }}
</span>
</td>
<td class="py-2 text-slate-600">{{ p.kategorie|default:"—" }}</td>
<td class="py-2 text-slate-500 text-xs">{{ p.erfassungsdatum }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-slate-500 text-sm">Noch keine Passagen erfasst.</p>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block title %}{% if neu %}Neuer Marktbegleiter{% else %}{{ obj.name }} bearbeiten{% endif %}{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-5">
<h1 class="page-title">{% if neu %}Neuer Marktbegleiter{% else %}{{ obj.name }} bearbeiten{% endif %}</h1>
{% if not neu %}
<a href="{% url 'marktbegleiter:detail' pk=obj.pk %}" class="btn-secondary">Abbrechen</a>
{% else %}
<a href="{% url 'marktbegleiter:liste' %}" class="btn-secondary">Abbrechen</a>
{% endif %}
</div>
<div class="card max-w-3xl">
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-4">
<label class="form-label">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
<p class="text-red-500 text-xs mt-1">{{ field.errors|join:", " }}</p>
{% endif %}
</div>
{% endfor %}
<div class="flex gap-3 mt-6">
<button type="submit" class="btn-primary">Speichern</button>
</div>
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block title %}Marktbegleiter{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-5">
<h1 class="page-title">Marktbegleiter</h1>
<a href="{% url 'marktbegleiter:neu' %}" class="btn-primary">+ Neu</a>
</div>
<div class="card mb-4">
<form method="get" class="flex flex-wrap gap-3 items-end">
<div>
<label class="form-label">Name</label>
<input type="text" name="q" value="{{ q }}" class="form-input" placeholder="Suche...">
</div>
<div>
<label class="form-label">Branche</label>
<input type="text" name="branche" value="{{ branche }}" class="form-input" placeholder="Branche...">
</div>
<button type="submit" class="btn-secondary">Filtern</button>
</form>
</div>
<div class="card">
{% if marktbegleiter_liste %}
<table class="w-full text-sm">
<thead>
<tr class="text-left border-b border-slate-200">
<th class="pb-2 font-medium text-slate-600">Name</th>
<th class="pb-2 font-medium text-slate-600">Branchen</th>
<th class="pb-2 font-medium text-slate-600">Vertraulichkeit</th>
<th class="pb-2 font-medium text-slate-600">Aktualisiert</th>
<th></th>
</tr>
</thead>
<tbody>
{% for mb in marktbegleiter_liste %}
<tr class="border-b border-slate-100 hover:bg-slate-50">
<td class="py-2 font-medium">
<a href="{% url 'marktbegleiter:detail' pk=mb.pk %}" class="text-brand-600 hover:underline">{{ mb.name }}</a>
</td>
<td class="py-2 text-slate-600 max-w-xs truncate">{{ mb.relevante_branchen|truncatechars:80|default:"—" }}</td>
<td class="py-2 text-slate-600">{{ mb.get_vertraulichkeit_display }}</td>
<td class="py-2 text-slate-500 text-xs">{{ mb.letzte_aktualisierung|default:"—" }}</td>
<td class="py-2 text-right">
<a href="{% url 'marktbegleiter:bearbeiten' pk=mb.pk %}" class="text-slate-400 hover:text-slate-700 text-xs">Bearbeiten</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-slate-500 text-sm">Keine Marktbegleiter gefunden.</p>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block title %}Passage — {{ passage.marktbegleiter.name }}{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-5">
<h1 class="page-title">Passage: {{ passage.marktbegleiter.name }}</h1>
<div class="flex gap-2">
<a href="{% url 'marktbegleiter:passagen:bearbeiten' ausschreibung_id=ausschreibung.pk pk=passage.pk %}" class="btn-primary">Bearbeiten</a>
<a href="{% url 'marktbegleiter:passagen:liste' ausschreibung_id=ausschreibung.pk %}" class="btn-secondary">Zurück</a>
</div>
</div>
<div class="card max-w-2xl space-y-4">
<div>
<p class="text-xs text-slate-500 mb-1">Passage</p>
<p class="text-sm text-slate-700 whitespace-pre-line">{{ passage.passage }}</p>
</div>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<p class="text-xs text-slate-500">Verlässlichkeitsscore</p>
<p class="font-bold text-lg {% if passage.verlaesslichkeitsscore >= 7 %}text-green-600{% elif passage.verlaesslichkeitsscore >= 4 %}text-yellow-600{% else %}text-red-500{% endif %}">
{{ passage.verlaesslichkeitsscore }}/10
</p>
</div>
<div>
<p class="text-xs text-slate-500">Kategorie</p>
<p>{{ passage.kategorie|default:"—" }}</p>
</div>
<div>
<p class="text-xs text-slate-500">Dokument</p>
<p>{{ passage.dokument|default:"—" }}</p>
</div>
<div>
<p class="text-xs text-slate-500">Fundstelle</p>
<p>{{ passage.fundstelle|default:"—" }}</p>
</div>
<div>
<p class="text-xs text-slate-500">Erfasst am</p>
<p>{{ passage.erfassungsdatum }}</p>
</div>
</div>
{% if passage.begruendung_zuordnung %}
<div>
<p class="text-xs text-slate-500">Begründung</p>
<p class="text-sm text-slate-700">{{ passage.begruendung_zuordnung }}</p>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}{% if neu %}Neue Passage{% else %}Passage bearbeiten{% endif %}{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-5">
<h1 class="page-title">{% if neu %}Neue Passage{% else %}Passage bearbeiten{% endif %}</h1>
<a href="{% url 'marktbegleiter:passagen:liste' ausschreibung_id=ausschreibung.pk %}" class="btn-secondary">Abbrechen</a>
</div>
<p class="text-sm text-slate-500 mb-5">Ausschreibung: <strong>{{ ausschreibung.titel }}</strong></p>
<div class="card max-w-3xl" x-data="{ score: {{ passage.verlaesslichkeitsscore|default:5 }} }">
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-4">
<label class="form-label">{{ field.label }}</label>
{% if field.name == 'verlaesslichkeitsscore' %}
<div class="flex items-center gap-3">
{{ field }}
<span class="text-sm font-medium text-brand-600" x-text="score"></span>
<span class="text-xs text-slate-400">(1=sehr unsicher, 10=sehr sicher)</span>
</div>
{% else %}
{{ field }}
{% endif %}
{% if field.errors %}
<p class="text-red-500 text-xs mt-1">{{ field.errors|join:", " }}</p>
{% endif %}
</div>
{% endfor %}
<div class="flex gap-3 mt-6">
<button type="submit" class="btn-primary">Speichern</button>
</div>
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,52 @@
{% extends "base.html" %}
{% block title %}Marktbegleiter-Passagen — {{ ausschreibung.titel }}{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-5">
<h1 class="page-title">Marktbegleiter-Passagen</h1>
<div class="flex gap-2">
<span class="text-sm text-slate-500 self-center">{{ ausschreibung.titel|truncatechars:50 }}</span>
<a href="{% url 'marktbegleiter:passagen:neu' ausschreibung_id=ausschreibung.pk %}" class="btn-primary">+ Neue Passage</a>
</div>
</div>
<div class="card">
{% if passagen %}
<table class="w-full text-sm">
<thead>
<tr class="text-left border-b border-slate-200">
<th class="pb-2 font-medium text-slate-600">Marktbegleiter</th>
<th class="pb-2 font-medium text-slate-600">Textauszug</th>
<th class="pb-2 font-medium text-slate-600">Score</th>
<th class="pb-2 font-medium text-slate-600">Kategorie</th>
<th class="pb-2 font-medium text-slate-600">Dokument</th>
<th class="pb-2 font-medium text-slate-600">Datum</th>
<th></th>
</tr>
</thead>
<tbody>
{% for p in passagen %}
<tr class="border-b border-slate-100 hover:bg-slate-50">
<td class="py-2 font-medium">
<a href="{% url 'marktbegleiter:detail' pk=p.marktbegleiter.pk %}" class="text-brand-600 hover:underline">
{{ p.marktbegleiter.name }}
</a>
</td>
<td class="py-2 text-slate-600 max-w-xs">{{ p.passage|truncatechars:150 }}</td>
<td class="py-2 font-medium {% if p.verlaesslichkeitsscore >= 7 %}text-green-600{% elif p.verlaesslichkeitsscore >= 4 %}text-yellow-600{% else %}text-red-500{% endif %}">
{{ p.verlaesslichkeitsscore }}
</td>
<td class="py-2 text-slate-600">{{ p.kategorie|default:"—" }}</td>
<td class="py-2 text-slate-500 text-xs">{{ p.dokument.dateiname|default:"—" }}</td>
<td class="py-2 text-slate-500 text-xs">{{ p.erfassungsdatum }}</td>
<td class="py-2 text-right">
<a href="{% url 'marktbegleiter:passagen:bearbeiten' ausschreibung_id=ausschreibung.pk pk=p.pk %}" class="text-slate-400 hover:text-slate-700 text-xs">Bearbeiten</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-slate-500 text-sm">Noch keine Passagen für diese Ausschreibung erfasst.</p>
{% endif %}
</div>
{% endblock %}