feat(partner,bibliothek): Subunternehmer-Katalog, Dienstleistertypen und Bibliothek (WP-0010)
Implementiert Subunternehmer-Katalog mit Suche/Filter, Zuordnung zu Losen via HTMX-Modal, Dienstleistertyp-CRUD und Präferenz-Badges. Bibliothek: Nachweis-Katalog mit Ablaufwarnung und Versionierung, Referenz-Katalog mit Ausschreibungszuordnung, Leistungsblatt-CRUD, Entscheidungsregel-CRUD mit Aktiv-Toggle. Migration für referenzen M2M auf Ausschreibung. 56 Tests grün. Tests-Discovery auf tests.py-Dateien ausgedehnt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ca8818254b
commit
278cc1014c
31 changed files with 1818 additions and 22 deletions
|
|
@ -0,0 +1,22 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}{% if obj %}{{ obj.name }} bearbeiten{% else %}Neuer Dienstleistertyp{% endif %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-2xl">
|
||||
<h1 class="page-title mb-6">{% if obj %}Bearbeiten: {{ obj.name }}{% else %}Neuer Dienstleistertyp{% endif %}</h1>
|
||||
|
||||
<form method="post" class="card space-y-4">
|
||||
{% csrf_token %}
|
||||
<div><label class="form-label">Name *</label>{{ form.name }}</div>
|
||||
<div><label class="form-label">Beschreibung</label>{{ form.beschreibung }}</div>
|
||||
<div><label class="form-label">Typische Leistungen</label>{{ form.typische_leistungen }}</div>
|
||||
<div><label class="form-label">Typische Nachweise</label>{{ form.typische_nachweise }}</div>
|
||||
<div><label class="form-label">Relevante Standards</label>{{ form.relevante_standards }}</div>
|
||||
<div><label class="form-label">Typische Preisbestandteile</label>{{ form.typische_preisbestandteile }}</div>
|
||||
<div><label class="form-label">Bemerkungen</label>{{ form.bemerkungen }}</div>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<a href="{% url 'partner:dt_liste' %}" class="btn-secondary">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Dienstleistertypen{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h1 class="page-title">Dienstleistertypen</h1>
|
||||
<a href="{% url 'partner:dt_neu' %}" class="btn-primary">+ Neu</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{% if typen %}
|
||||
<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">Beschreibung</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Subunternehmer</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for typ in typen %}
|
||||
<tr class="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td class="py-2 font-medium">{{ typ.name }}</td>
|
||||
<td class="py-2 text-slate-600">{{ typ.beschreibung|truncatechars:60|default:"—" }}</td>
|
||||
<td class="py-2 text-slate-600">{{ typ.subunternehmer.count }}</td>
|
||||
<td class="py-2 text-right">
|
||||
<a href="{% url 'partner:dt_bearbeiten' pk=typ.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 Dienstleistertypen angelegt.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{% if sub.praeferenz == 'bevorzugt' %}
|
||||
<span class="badge-green">Bevorzugt</span>
|
||||
{% elif sub.praeferenz == 'gesperrt' %}
|
||||
<span class="badge-red">Gesperrt</span>
|
||||
{% else %}
|
||||
<span class="badge-gray">Zugelassen</span>
|
||||
{% endif %}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<div>
|
||||
<div class="mb-3">
|
||||
<input type="text" name="q" value="{{ q }}"
|
||||
class="form-input w-full"
|
||||
placeholder="Subunternehmer suchen..."
|
||||
hx-get="{% url 'partner:su_suche_modal' ausschreibung_id=ausschreibung_id los_pk=los_pk %}"
|
||||
hx-target="closest div"
|
||||
hx-trigger="input changed delay:300ms"
|
||||
hx-include="[name='q']">
|
||||
</div>
|
||||
|
||||
{% if subunternehmer %}
|
||||
<ul class="space-y-1 max-h-80 overflow-y-auto">
|
||||
{% for sub in subunternehmer %}
|
||||
<li class="flex items-center justify-between p-2 rounded hover:bg-slate-50 border border-slate-100">
|
||||
<div>
|
||||
<span class="font-medium text-sm">{{ sub.name }}</span>
|
||||
<span class="text-xs text-slate-500 ml-2">{{ sub.dienstleistertyp|default:"" }}</span>
|
||||
{% if sub.praeferenz == 'gesperrt' %}
|
||||
<span class="badge-red ml-2 text-xs">⚠ Gesperrt</span>
|
||||
{% elif sub.praeferenz == 'bevorzugt' %}
|
||||
<span class="badge-green ml-2 text-xs">Bevorzugt</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<form hx-post="{% url 'partner:su_zuordnen' ausschreibung_id=ausschreibung_id los_pk=los_pk %}"
|
||||
hx-target="#zuordnungen-liste"
|
||||
hx-swap="beforeend"
|
||||
hx-on::after-request="document.getElementById('su-modal').remove()">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="subunternehmer_id" value="{{ sub.pk }}">
|
||||
<button type="submit" class="btn-primary btn-sm">Zuordnen</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-slate-500 text-sm">Kein Subunternehmer gefunden.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<tr class="border-b border-slate-100" id="zuordnung-{{ zuordnung.pk }}">
|
||||
<td class="py-2 font-medium text-sm">{{ zuordnung.subunternehmer.name }}</td>
|
||||
<td class="py-2 text-sm text-slate-600">{{ zuordnung.subunternehmer.dienstleistertyp|default:"—" }}</td>
|
||||
<td class="py-2 text-sm text-slate-600">{{ zuordnung.konkrete_leistung|default:"—" }}</td>
|
||||
<td class="py-2">
|
||||
<form hx-post="{% url 'partner:zuordnung_toggle' pk=zuordnung.pk %}"
|
||||
hx-target="#zuordnung-{{ zuordnung.pk }}"
|
||||
hx-swap="outerHTML">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="feld" value="zusage_vorhanden">
|
||||
<button type="submit" class="text-lg {% if zuordnung.zusage_vorhanden %}text-green-600{% else %}text-slate-300{% endif %}">
|
||||
{% if zuordnung.zusage_vorhanden %}✓{% else %}○{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="py-2">
|
||||
<form hx-post="{% url 'partner:zuordnung_toggle' pk=zuordnung.pk %}"
|
||||
hx-target="#zuordnung-{{ zuordnung.pk }}"
|
||||
hx-swap="outerHTML">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="feld" value="nachweis_eingegangen">
|
||||
<button type="submit" class="text-lg {% if zuordnung.nachweis_eingegangen %}text-green-600{% else %}text-slate-300{% endif %}">
|
||||
{% if zuordnung.nachweis_eingegangen %}✓{% else %}○{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="py-2">
|
||||
<form hx-post="{% url 'partner:zuordnung_toggle' pk=zuordnung.pk %}"
|
||||
hx-target="#zuordnung-{{ zuordnung.pk }}"
|
||||
hx-swap="outerHTML">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="feld" value="preis_vorhanden">
|
||||
<button type="submit" class="text-lg {% if zuordnung.preis_vorhanden %}text-green-600{% else %}text-slate-300{% endif %}">
|
||||
{% if zuordnung.preis_vorhanden %}✓{% else %}○{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
102
vergabe_teilnahme/templates/partner/subunternehmer_detail.html
Normal file
102
vergabe_teilnahme/templates/partner/subunternehmer_detail.html
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}{{ sub.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex items-start justify-between mb-5">
|
||||
<div>
|
||||
<h1 class="page-title">{{ sub.name }}</h1>
|
||||
<p class="text-slate-500 text-sm">{{ sub.dienstleistertyp|default:"Kein Dienstleistertyp" }}</p>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div id="praeferenz-badge"
|
||||
hx-get="{% url 'partner:su_praeferenz' pk=sub.pk %}"
|
||||
hx-trigger="load"
|
||||
hx-swap="outerHTML">
|
||||
{% if sub.praeferenz == 'bevorzugt' %}
|
||||
<span class="badge-green">Bevorzugt</span>
|
||||
{% elif sub.praeferenz == 'gesperrt' %}
|
||||
<span class="badge-red">Gesperrt</span>
|
||||
{% else %}
|
||||
<span class="badge-gray">Zugelassen</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'partner:su_bearbeiten' pk=sub.pk %}" class="btn-secondary">Bearbeiten</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 mb-6">
|
||||
<div class="card col-span-2 space-y-3">
|
||||
<h2 class="section-title">Kontakt</h2>
|
||||
{% if sub.strasse or sub.ort %}
|
||||
<p class="text-sm">{{ sub.strasse }}{% if sub.strasse and sub.ort %}, {% endif %}{{ sub.plz }} {{ sub.ort }}, {{ sub.land }}</p>
|
||||
{% endif %}
|
||||
{% if sub.telefon %}<p class="text-sm">Tel: {{ sub.telefon }}</p>{% endif %}
|
||||
{% if sub.email %}<p class="text-sm">E-Mail: <a href="mailto:{{ sub.email }}" class="text-brand-600">{{ sub.email }}</a></p>{% endif %}
|
||||
{% if sub.website %}<p class="text-sm"><a href="{{ sub.website }}" class="text-brand-600" target="_blank">{{ sub.website }}</a></p>{% endif %}
|
||||
{% if sub.ansprechpartner %}
|
||||
<p class="text-sm font-medium">Ansprechpartner: {{ sub.ansprechpartner }}
|
||||
{% if sub.ansprechpartner_email %} — <a href="mailto:{{ sub.ansprechpartner_email }}" class="text-brand-600">{{ sub.ansprechpartner_email }}</a>{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="section-title mb-3">Präferenz ändern</h2>
|
||||
<form hx-post="{% url 'partner:su_praeferenz' pk=sub.pk %}"
|
||||
hx-target="#praeferenz-badge"
|
||||
hx-swap="outerHTML"
|
||||
class="space-y-2">
|
||||
{% csrf_token %}
|
||||
{% for val, label in sub.PRAEFERENZ_CHOICES %}
|
||||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input type="radio" name="praeferenz" value="{{ val }}" {% if sub.praeferenz == val %}checked{% endif %}>
|
||||
{{ label }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
<div id="begruendung-field" class="mt-2{% if sub.praeferenz != 'gesperrt' %} hidden{% endif %}">
|
||||
<label class="form-label">Begründung</label>
|
||||
<textarea name="begruendung" class="form-input" rows="2">{{ sub.bewertung }}</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn-secondary btn-sm w-full mt-2">Speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if sub.leistungsprofil %}
|
||||
<div class="card mb-4">
|
||||
<h2 class="section-title mb-2">Leistungsprofil</h2>
|
||||
<p class="text-sm text-slate-700 whitespace-pre-line">{{ sub.leistungsprofil }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<h2 class="section-title mb-3">Ausschreibungs-Zuordnungen</h2>
|
||||
{% if zuordnungen %}
|
||||
<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">Los</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Leistung</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Zusage</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Nachweis</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Preis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for z in zuordnungen %}
|
||||
<tr class="border-b border-slate-100">
|
||||
<td class="py-2">{{ z.ausschreibung.titel|truncatechars:50 }}</td>
|
||||
<td class="py-2">{{ z.los|default:"—" }}</td>
|
||||
<td class="py-2 text-slate-600">{{ z.konkrete_leistung|default:"—" }}</td>
|
||||
<td class="py-2">{% if z.zusage_vorhanden %}<span class="badge-green">✓</span>{% else %}<span class="text-slate-300">—</span>{% endif %}</td>
|
||||
<td class="py-2">{% if z.nachweis_eingegangen %}<span class="badge-green">✓</span>{% else %}<span class="text-slate-300">—</span>{% endif %}</td>
|
||||
<td class="py-2">{% if z.preis_vorhanden %}<span class="badge-green">✓</span>{% else %}<span class="text-slate-300">—</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-slate-500 text-sm">Noch keiner Ausschreibung zugeordnet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
65
vergabe_teilnahme/templates/partner/subunternehmer_form.html
Normal file
65
vergabe_teilnahme/templates/partner/subunternehmer_form.html
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}{% if sub %}{{ sub.name }} bearbeiten{% else %}Neuer Subunternehmer{% endif %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-3xl">
|
||||
<h1 class="page-title mb-6">{% if sub %}{{ sub.name }} bearbeiten{% else %}Neuer Subunternehmer{% endif %}</h1>
|
||||
|
||||
<form method="post" class="space-y-6">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card space-y-4">
|
||||
<h2 class="section-title">Stammdaten</h2>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div><label class="form-label">Name *</label>{{ form.name }}</div>
|
||||
<div><label class="form-label">Kurzname</label>{{ form.kurzname }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div><label class="form-label">Dienstleistertyp</label>{{ form.dienstleistertyp }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Präferenz</label>
|
||||
<div class="flex gap-4 mt-1">{{ form.praeferenz }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card space-y-4">
|
||||
<h2 class="section-title">Kontakt</h2>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="col-span-2"><label class="form-label">Straße</label>{{ form.strasse }}</div>
|
||||
<div><label class="form-label">PLZ</label>{{ form.plz }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div><label class="form-label">Ort</label>{{ form.ort }}</div>
|
||||
<div><label class="form-label">Land</label>{{ form.land }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div><label class="form-label">Telefon</label>{{ form.telefon }}</div>
|
||||
<div><label class="form-label">Mobil</label>{{ form.mobilnummer }}</div>
|
||||
<div><label class="form-label">E-Mail</label>{{ form.email }}</div>
|
||||
</div>
|
||||
<div><label class="form-label">Website</label>{{ form.website }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card space-y-4">
|
||||
<h2 class="section-title">Ansprechpartner</h2>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div><label class="form-label">Name</label>{{ form.ansprechpartner }}</div>
|
||||
<div><label class="form-label">E-Mail</label>{{ form.ansprechpartner_email }}</div>
|
||||
<div><label class="form-label">Telefon</label>{{ form.ansprechpartner_telefon }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card space-y-4">
|
||||
<h2 class="section-title">Profil & Bewertung</h2>
|
||||
<div><label class="form-label">Leistungsprofil</label>{{ form.leistungsprofil }}</div>
|
||||
<div><label class="form-label">Bewertung / Begründung</label>{{ form.bewertung }}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<a href="{% if sub %}{% url 'partner:su_detail' pk=sub.pk %}{% else %}{% url 'partner:su_liste' %}{% endif %}"
|
||||
class="btn-secondary">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Subunternehmer{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h1 class="page-title">Subunternehmer</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="{% url 'partner:dt_liste' %}" class="btn-secondary">Dienstleistertypen</a>
|
||||
<a href="{% url 'partner:su_neu' %}" class="btn-primary">+ Neu</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<form method="get" class="flex flex-wrap gap-3 items-end">
|
||||
<div>
|
||||
<label class="form-label">Suche</label>
|
||||
<input type="text" name="q" value="{{ q }}" class="form-input" placeholder="Name...">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Präferenz</label>
|
||||
<select name="praeferenz" class="form-input">
|
||||
<option value="">Alle</option>
|
||||
{% for val, label in praeferenz_choices %}
|
||||
<option value="{{ val }}" {% if current_praeferenz == val %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Dienstleistertyp</label>
|
||||
<select name="dienstleistertyp" class="form-input">
|
||||
<option value="">Alle</option>
|
||||
{% for dt in dienstleistertypen %}
|
||||
<option value="{{ dt.pk }}" {% if current_dt == dt.pk|stringformat:"s" %}selected{% endif %}>{{ dt.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-secondary">Filtern</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{% if subunternehmer %}
|
||||
<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">Dienstleistertyp</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Präferenz</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Ort</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for sub in subunternehmer %}
|
||||
<tr class="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td class="py-2 font-medium">
|
||||
<a href="{% url 'partner:su_detail' pk=sub.pk %}" class="text-brand-600 hover:underline">
|
||||
{{ sub.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="py-2 text-slate-600">{{ sub.dienstleistertyp|default:"—" }}</td>
|
||||
<td class="py-2">
|
||||
{% if sub.praeferenz == 'bevorzugt' %}
|
||||
<span class="badge-green">Bevorzugt</span>
|
||||
{% elif sub.praeferenz == 'gesperrt' %}
|
||||
<span class="badge-red">Gesperrt</span>
|
||||
{% else %}
|
||||
<span class="badge-gray">Zugelassen</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="py-2 text-slate-600">{{ sub.ort|default:"—" }}</td>
|
||||
<td class="py-2 text-right">
|
||||
<a href="{% url 'partner:su_bearbeiten' pk=sub.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 Subunternehmer gefunden.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue