generated from coulomb/repo-seed
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:
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-11 13:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ausschreibungen', '0002_add_archiviert'),
|
||||
('bibliothek', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ausschreibung',
|
||||
name='referenzen',
|
||||
field=models.ManyToManyField(blank=True, related_name='ausschreibungen', to='bibliothek.referenz'),
|
||||
),
|
||||
]
|
||||
@@ -69,6 +69,10 @@ class Ausschreibung(FlexibleModel):
|
||||
laufzeit = models.CharField(max_length=100, blank=True)
|
||||
optionen = models.TextField(blank=True)
|
||||
|
||||
referenzen = models.ManyToManyField(
|
||||
'bibliothek.Referenz', blank=True, related_name='ausschreibungen'
|
||||
)
|
||||
|
||||
# Herkunft & Dokumente
|
||||
fundstelle_url = models.URLField(max_length=1000, blank=True)
|
||||
unterlagen_erhalten = models.BooleanField(default=False)
|
||||
|
||||
@@ -1,3 +1,74 @@
|
||||
from django.test import TestCase
|
||||
from datetime import date, timedelta
|
||||
|
||||
# Create your tests here.
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from .models import Entscheidungsregel, Nachweis, Referenz
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_nachweis_ist_abgelaufen_true():
|
||||
n = Nachweis.objects.create(titel='Alter Nachweis', gueltig_bis=date.today() - timedelta(days=1))
|
||||
assert n.ist_abgelaufen is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_nachweis_ist_abgelaufen_false_without_date():
|
||||
n = Nachweis.objects.create(titel='Nachweis ohne Datum')
|
||||
assert n.ist_abgelaufen is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_nachweis_liste_filter_abgelaufen(client):
|
||||
heute = date.today()
|
||||
Nachweis.objects.create(titel='AbgelaufenerNachweis', gueltig_bis=heute - timedelta(days=5))
|
||||
Nachweis.objects.create(titel='NochAktuellerNachweis', gueltig_bis=heute + timedelta(days=100))
|
||||
url = reverse('bibliothek:nachweise_liste')
|
||||
response = client.get(url + '?tab=abgelaufen')
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode()
|
||||
assert 'AbgelaufenerNachweis' in content
|
||||
assert 'NochAktuellerNachweis' not in content
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_entscheidungsregel_inaktiv_nicht_in_liste(client):
|
||||
Entscheidungsregel.objects.create(
|
||||
regelname='Aktive Regel', kategorie='ausschlusskriterium', empfehlung='teilnehmen', aktiv=True
|
||||
)
|
||||
Entscheidungsregel.objects.create(
|
||||
regelname='Inaktive Regel', kategorie='frist', empfehlung='pruefen', aktiv=False
|
||||
)
|
||||
url = reverse('bibliothek:entscheidungsregeln_liste')
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode()
|
||||
assert 'Aktive Regel' in content
|
||||
assert 'Inaktive Regel' in content
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_entscheidungsregel_toggle(client):
|
||||
r = Entscheidungsregel.objects.create(
|
||||
regelname='Toggle-Regel', kategorie='ausschlusskriterium', empfehlung='teilnehmen', aktiv=True
|
||||
)
|
||||
url = reverse('bibliothek:er_toggle', kwargs={'pk': r.pk})
|
||||
client.post(url)
|
||||
r.refresh_from_db()
|
||||
assert r.aktiv is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_nachweis_neue_version_ersetzt_alten(client):
|
||||
alt = Nachweis.objects.create(titel='Zertifikat ISO', version='1.0')
|
||||
url = reverse('bibliothek:nachweis_version', kwargs={'pk': alt.pk})
|
||||
client.post(url, {
|
||||
'titel': 'Zertifikat ISO',
|
||||
'version': '1.0',
|
||||
'sprache': 'de',
|
||||
'freigabestatus': 'intern_freigegeben',
|
||||
'vertraulichkeit': 'intern',
|
||||
})
|
||||
alt.refresh_from_db()
|
||||
assert alt.status == 'ersetzt'
|
||||
assert Nachweis.objects.filter(titel='Zertifikat ISO').count() == 2
|
||||
|
||||
@@ -1,2 +1,25 @@
|
||||
from django.urls import path
|
||||
urlpatterns = []
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'bibliothek'
|
||||
|
||||
urlpatterns = [
|
||||
path('nachweise/', views.nachweise_liste, name='nachweise_liste'),
|
||||
path('nachweise/neu/', views.nachweis_neu, name='nachweis_neu'),
|
||||
path('nachweise/<int:pk>/', views.nachweis_detail, name='nachweis_detail'),
|
||||
path('nachweise/<int:pk>/bearbeiten/', views.nachweis_bearbeiten, name='nachweis_bearbeiten'),
|
||||
path('nachweise/<int:pk>/version/', views.nachweis_neue_version, name='nachweis_version'),
|
||||
path('referenzen/', views.referenzen_liste, name='referenz_liste'),
|
||||
path('referenzen/neu/', views.referenz_neu, name='referenz_neu'),
|
||||
path('referenzen/<int:pk>/', views.referenz_detail, name='referenz_detail'),
|
||||
path('referenzen/<int:pk>/bearbeiten/', views.referenz_bearbeiten, name='referenz_bearbeiten'),
|
||||
path('referenzen/zuordnen/<int:ausschreibung_id>/', views.referenz_zuordnen, name='referenz_zuordnen'),
|
||||
path('leistungsblaetter/', views.leistungsblaetter_liste, name='leistungsblaetter_liste'),
|
||||
path('leistungsblaetter/neu/', views.leistungsblatt_neu, name='leistungsblatt_neu'),
|
||||
path('leistungsblaetter/<int:pk>/bearbeiten/', views.leistungsblatt_bearbeiten, name='leistungsblatt_bearbeiten'),
|
||||
path('entscheidungsregeln/', views.entscheidungsregeln_liste, name='entscheidungsregeln_liste'),
|
||||
path('entscheidungsregeln/neu/', views.entscheidungsregel_neu, name='entscheidungsregel_neu'),
|
||||
path('entscheidungsregeln/<int:pk>/bearbeiten/', views.entscheidungsregel_bearbeiten, name='entscheidungsregel_bearbeiten'),
|
||||
path('entscheidungsregeln/<int:pk>/toggle/', views.entscheidungsregel_toggle, name='er_toggle'),
|
||||
]
|
||||
|
||||
@@ -1,3 +1,450 @@
|
||||
from django.shortcuts import render
|
||||
from datetime import date, timedelta
|
||||
|
||||
# Create your views here.
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from vergabe_teilnahme.apps.accounts.models import Mitarbeiter
|
||||
|
||||
from .models import Entscheidungsregel, Leistungsblatt, Nachweis, Referenz
|
||||
|
||||
|
||||
# ── Forms ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
class NachweisForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Nachweis
|
||||
fields = [
|
||||
'titel', 'kurzbeschreibung', 'dokumenttyp', 'kategorie',
|
||||
'datei', 'version', 'gueltig_ab', 'gueltig_bis',
|
||||
'eigentuemer', 'freigabestatus', 'vertraulichkeit',
|
||||
'sprache', 'zugehoerige_standards', 'letzte_pruefung',
|
||||
'fuer_oeffentliche', 'fuer_privatwirtschaftliche',
|
||||
]
|
||||
widgets = {
|
||||
'titel': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'kurzbeschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'dokumenttyp': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'kategorie': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'version': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'gueltig_ab': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}),
|
||||
'gueltig_bis': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}),
|
||||
'eigentuemer': forms.Select(attrs={'class': 'form-select'}),
|
||||
'freigabestatus': forms.Select(attrs={'class': 'form-select'}),
|
||||
'vertraulichkeit': forms.Select(attrs={'class': 'form-select'}),
|
||||
'sprache': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'zugehoerige_standards': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||
'letzte_pruefung': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['eigentuemer'].queryset = Mitarbeiter.objects.all()
|
||||
self.fields['eigentuemer'].required = False
|
||||
self.fields['gueltig_ab'].required = False
|
||||
self.fields['gueltig_bis'].required = False
|
||||
self.fields['letzte_pruefung'].required = False
|
||||
|
||||
|
||||
class ReferenzForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Referenz
|
||||
fields = [
|
||||
'referenztitel', 'kunde', 'branche', 'oeffentlich_oder_privat',
|
||||
'leistungsbeschreibung', 'eingesetzte_produkte', 'projektzeitraum',
|
||||
'vertragsvolumen', 'ansprechpartner_referenzkunde',
|
||||
'freigabestatus_verwendung', 'vertraulichkeit', 'whitepaper',
|
||||
'kurzfassung', 'langfassung', 'verwendbar_fuer_ausschreibungen',
|
||||
'einschraenkungen_verwendung', 'leistungsblaetter',
|
||||
]
|
||||
widgets = {
|
||||
'referenztitel': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'kunde': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'branche': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'oeffentlich_oder_privat': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'leistungsbeschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 4}),
|
||||
'eingesetzte_produkte': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'projektzeitraum': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'vertragsvolumen': forms.NumberInput(attrs={'class': 'form-input'}),
|
||||
'ansprechpartner_referenzkunde': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'freigabestatus_verwendung': forms.Select(attrs={'class': 'form-select'}),
|
||||
'vertraulichkeit': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'kurzfassung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'langfassung': forms.Textarea(attrs={'class': 'form-input', 'rows': 5}),
|
||||
'einschraenkungen_verwendung': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||
'leistungsblaetter': forms.CheckboxSelectMultiple(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['vertragsvolumen'].required = False
|
||||
self.fields['leistungsblaetter'].required = False
|
||||
|
||||
|
||||
class LeistungsblattForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Leistungsblatt
|
||||
fields = [
|
||||
'produktfunktion', 'beschreibung', 'leistungsumfang',
|
||||
'grenzen_ausschluesse', 'technische_voraussetzungen',
|
||||
'typische_nachweise', 'version', 'eigentuemer',
|
||||
]
|
||||
widgets = {
|
||||
'produktfunktion': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'leistungsumfang': forms.Textarea(attrs={'class': 'form-input', 'rows': 4}),
|
||||
'grenzen_ausschluesse': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'technische_voraussetzungen': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'typische_nachweise': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||
'version': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'eigentuemer': forms.Select(attrs={'class': 'form-select'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['eigentuemer'].queryset = Mitarbeiter.objects.all()
|
||||
self.fields['eigentuemer'].required = False
|
||||
|
||||
|
||||
class EntscheidungsregelForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Entscheidungsregel
|
||||
fields = [
|
||||
'regelname', 'beschreibung', 'kategorie', 'gewichtung',
|
||||
'bewertungslogik', 'schwellenwert', 'empfehlung', 'begruendung',
|
||||
'gueltig_von', 'gueltig_bis', 'aktiv', 'verantwortlicher',
|
||||
]
|
||||
widgets = {
|
||||
'regelname': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'kategorie': forms.Select(attrs={'class': 'form-select'}),
|
||||
'gewichtung': forms.NumberInput(attrs={'class': 'form-input', 'step': '0.1'}),
|
||||
'bewertungslogik': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'schwellenwert': forms.NumberInput(attrs={'class': 'form-input'}),
|
||||
'empfehlung': forms.RadioSelect(),
|
||||
'begruendung': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||
'gueltig_von': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}),
|
||||
'gueltig_bis': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}),
|
||||
'verantwortlicher': forms.Select(attrs={'class': 'form-select'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['verantwortlicher'].queryset = Mitarbeiter.objects.all()
|
||||
self.fields['verantwortlicher'].required = False
|
||||
self.fields['schwellenwert'].required = False
|
||||
self.fields['gueltig_von'].required = False
|
||||
self.fields['gueltig_bis'].required = False
|
||||
|
||||
|
||||
# ── Nachweis views ────────────────────────────────────────────────────────────
|
||||
|
||||
def nachweise_liste(request):
|
||||
heute = date.today()
|
||||
in_60_tagen = heute + timedelta(days=60)
|
||||
|
||||
tab = request.GET.get('tab', 'alle')
|
||||
qs = Nachweis.objects.exclude(status='ersetzt')
|
||||
|
||||
if tab == 'abgelaufen':
|
||||
qs = qs.filter(gueltig_bis__lt=heute)
|
||||
elif tab == 'bald_ablaufend':
|
||||
qs = qs.filter(gueltig_bis__lte=in_60_tagen, gueltig_bis__gte=heute)
|
||||
|
||||
return render(request, 'bibliothek/nachweis_liste.html', {
|
||||
'nachweise': qs,
|
||||
'heute': heute,
|
||||
'in_60_tagen': in_60_tagen,
|
||||
'tab': tab,
|
||||
'breadcrumbs': [{'label': 'Nachweise', 'url': None}],
|
||||
})
|
||||
|
||||
|
||||
def nachweis_neu(request):
|
||||
if request.method == 'POST':
|
||||
form = NachweisForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
obj = form.save()
|
||||
messages.success(request, f'Nachweis „{obj.titel}" angelegt.')
|
||||
return redirect('bibliothek:nachweis_detail', pk=obj.pk)
|
||||
else:
|
||||
form = NachweisForm()
|
||||
|
||||
return render(request, 'bibliothek/nachweis_form.html', {
|
||||
'form': form,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Nachweise', 'url': '/bibliothek/nachweise/'},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def nachweis_detail(request, pk):
|
||||
nachweis = get_object_or_404(Nachweis, pk=pk)
|
||||
return render(request, 'bibliothek/nachweis_detail.html', {
|
||||
'nachweis': nachweis,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Nachweise', 'url': '/bibliothek/nachweise/'},
|
||||
{'label': nachweis.titel, 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def nachweis_bearbeiten(request, pk):
|
||||
nachweis = get_object_or_404(Nachweis, pk=pk)
|
||||
if request.method == 'POST':
|
||||
form = NachweisForm(request.POST, request.FILES, instance=nachweis)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'Gespeichert.')
|
||||
return redirect('bibliothek:nachweis_detail', pk=pk)
|
||||
else:
|
||||
form = NachweisForm(instance=nachweis)
|
||||
|
||||
return render(request, 'bibliothek/nachweis_form.html', {
|
||||
'form': form,
|
||||
'nachweis': nachweis,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Nachweise', 'url': '/bibliothek/nachweise/'},
|
||||
{'label': nachweis.titel, 'url': f'/bibliothek/nachweise/{pk}/'},
|
||||
{'label': 'Bearbeiten', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def nachweis_neue_version(request, pk):
|
||||
alter_nachweis = get_object_or_404(Nachweis, pk=pk)
|
||||
if request.method == 'POST':
|
||||
form = NachweisForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
alter_nachweis.status = 'ersetzt'
|
||||
alter_nachweis.save(update_fields=['status'])
|
||||
neuer = form.save(commit=False)
|
||||
# Parse version for auto-increment
|
||||
try:
|
||||
major, minor = alter_nachweis.version.split('.')
|
||||
neuer.version = f'{major}.{int(minor) + 1}'
|
||||
except (ValueError, AttributeError):
|
||||
neuer.version = alter_nachweis.version
|
||||
neuer.save()
|
||||
messages.success(request, f'Neue Version {neuer.version} angelegt.')
|
||||
return redirect('bibliothek:nachweis_detail', pk=neuer.pk)
|
||||
else:
|
||||
form = NachweisForm(instance=alter_nachweis)
|
||||
|
||||
return render(request, 'bibliothek/nachweis_form.html', {
|
||||
'form': form,
|
||||
'nachweis': alter_nachweis,
|
||||
'neue_version': True,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Nachweise', 'url': '/bibliothek/nachweise/'},
|
||||
{'label': alter_nachweis.titel, 'url': f'/bibliothek/nachweise/{pk}/'},
|
||||
{'label': 'Neue Version', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
# ── Referenz views ────────────────────────────────────────────────────────────
|
||||
|
||||
def referenzen_liste(request):
|
||||
q = request.GET.get('q', '').strip()
|
||||
qs = Referenz.objects.all()
|
||||
if q:
|
||||
qs = qs.filter(
|
||||
referenztitel__icontains=q
|
||||
) | qs.filter(
|
||||
branche__icontains=q
|
||||
) | qs.filter(
|
||||
leistungsbeschreibung__icontains=q
|
||||
)
|
||||
qs = qs.distinct()
|
||||
|
||||
return render(request, 'bibliothek/referenz_liste.html', {
|
||||
'referenzen': qs,
|
||||
'q': q,
|
||||
'breadcrumbs': [{'label': 'Referenzen', 'url': None}],
|
||||
})
|
||||
|
||||
|
||||
def referenz_neu(request):
|
||||
if request.method == 'POST':
|
||||
form = ReferenzForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
obj = form.save()
|
||||
messages.success(request, f'Referenz „{obj.referenztitel}" angelegt.')
|
||||
return redirect('bibliothek:referenz_detail', pk=obj.pk)
|
||||
else:
|
||||
form = ReferenzForm()
|
||||
|
||||
return render(request, 'bibliothek/referenz_form.html', {
|
||||
'form': form,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Referenzen', 'url': '/bibliothek/referenzen/'},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def referenz_detail(request, pk):
|
||||
ref = get_object_or_404(Referenz, pk=pk)
|
||||
return render(request, 'bibliothek/referenz_detail.html', {
|
||||
'ref': ref,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Referenzen', 'url': '/bibliothek/referenzen/'},
|
||||
{'label': ref.referenztitel, 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def referenz_bearbeiten(request, pk):
|
||||
ref = get_object_or_404(Referenz, pk=pk)
|
||||
if request.method == 'POST':
|
||||
form = ReferenzForm(request.POST, request.FILES, instance=ref)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'Gespeichert.')
|
||||
return redirect('bibliothek:referenz_detail', pk=pk)
|
||||
else:
|
||||
form = ReferenzForm(instance=ref)
|
||||
|
||||
return render(request, 'bibliothek/referenz_form.html', {
|
||||
'form': form,
|
||||
'ref': ref,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Referenzen', 'url': '/bibliothek/referenzen/'},
|
||||
{'label': ref.referenztitel, 'url': f'/bibliothek/referenzen/{pk}/'},
|
||||
{'label': 'Bearbeiten', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def referenz_zuordnen(request, ausschreibung_id):
|
||||
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
ref_id = request.POST.get('referenz_id')
|
||||
ref = get_object_or_404(Referenz, pk=ref_id)
|
||||
ausschreibung.referenzen.add(ref)
|
||||
messages.success(request, f'Referenz „{ref.referenztitel}" zugeordnet.')
|
||||
return redirect('ausschreibungen:detail', pk=ausschreibung_id)
|
||||
|
||||
q = request.GET.get('q', '').strip()
|
||||
qs = Referenz.objects.filter(verwendbar_fuer_ausschreibungen=True)
|
||||
if q:
|
||||
qs = qs.filter(referenztitel__icontains=q) | qs.filter(branche__icontains=q)
|
||||
qs = qs.distinct()
|
||||
|
||||
return render(request, 'bibliothek/partials/referenz_suche.html', {
|
||||
'referenzen': qs,
|
||||
'ausschreibung': ausschreibung,
|
||||
'q': q,
|
||||
})
|
||||
|
||||
|
||||
# ── Leistungsblatt views ──────────────────────────────────────────────────────
|
||||
|
||||
def leistungsblaetter_liste(request):
|
||||
blaetter = Leistungsblatt.objects.all()
|
||||
return render(request, 'bibliothek/leistungsblatt_liste.html', {
|
||||
'blaetter': blaetter,
|
||||
'breadcrumbs': [{'label': 'Leistungsblätter', 'url': None}],
|
||||
})
|
||||
|
||||
|
||||
def leistungsblatt_neu(request):
|
||||
if request.method == 'POST':
|
||||
form = LeistungsblattForm(request.POST)
|
||||
if form.is_valid():
|
||||
obj = form.save()
|
||||
messages.success(request, f'Leistungsblatt „{obj.produktfunktion}" angelegt.')
|
||||
return redirect('bibliothek:leistungsblaetter_liste')
|
||||
else:
|
||||
form = LeistungsblattForm()
|
||||
|
||||
return render(request, 'bibliothek/leistungsblatt_form.html', {
|
||||
'form': form,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Leistungsblätter', 'url': '/bibliothek/leistungsblaetter/'},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def leistungsblatt_bearbeiten(request, pk):
|
||||
obj = get_object_or_404(Leistungsblatt, pk=pk)
|
||||
if request.method == 'POST':
|
||||
form = LeistungsblattForm(request.POST, instance=obj)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'Gespeichert.')
|
||||
return redirect('bibliothek:leistungsblaetter_liste')
|
||||
else:
|
||||
form = LeistungsblattForm(instance=obj)
|
||||
|
||||
return render(request, 'bibliothek/leistungsblatt_form.html', {
|
||||
'form': form,
|
||||
'obj': obj,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Leistungsblätter', 'url': '/bibliothek/leistungsblaetter/'},
|
||||
{'label': 'Bearbeiten', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
# ── Entscheidungsregel views ──────────────────────────────────────────────────
|
||||
|
||||
def entscheidungsregeln_liste(request):
|
||||
regeln = Entscheidungsregel.objects.all()
|
||||
return render(request, 'bibliothek/entscheidungsregel_liste.html', {
|
||||
'regeln': regeln,
|
||||
'breadcrumbs': [{'label': 'Entscheidungsregeln', 'url': None}],
|
||||
})
|
||||
|
||||
|
||||
def entscheidungsregel_neu(request):
|
||||
if request.method == 'POST':
|
||||
form = EntscheidungsregelForm(request.POST)
|
||||
if form.is_valid():
|
||||
obj = form.save()
|
||||
messages.success(request, f'Entscheidungsregel „{obj.regelname}" angelegt.')
|
||||
return redirect('bibliothek:entscheidungsregeln_liste')
|
||||
else:
|
||||
form = EntscheidungsregelForm()
|
||||
|
||||
return render(request, 'bibliothek/entscheidungsregel_form.html', {
|
||||
'form': form,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Entscheidungsregeln', 'url': '/bibliothek/entscheidungsregeln/'},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def entscheidungsregel_bearbeiten(request, pk):
|
||||
obj = get_object_or_404(Entscheidungsregel, pk=pk)
|
||||
if request.method == 'POST':
|
||||
form = EntscheidungsregelForm(request.POST, instance=obj)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'Gespeichert.')
|
||||
return redirect('bibliothek:entscheidungsregeln_liste')
|
||||
else:
|
||||
form = EntscheidungsregelForm(instance=obj)
|
||||
|
||||
return render(request, 'bibliothek/entscheidungsregel_form.html', {
|
||||
'form': form,
|
||||
'obj': obj,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Entscheidungsregeln', 'url': '/bibliothek/entscheidungsregeln/'},
|
||||
{'label': 'Bearbeiten', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def entscheidungsregel_toggle(request, pk):
|
||||
obj = get_object_or_404(Entscheidungsregel, pk=pk)
|
||||
if request.method == 'POST':
|
||||
obj.aktiv = not obj.aktiv
|
||||
obj.save(update_fields=['aktiv'])
|
||||
return render(request, 'bibliothek/partials/er_aktiv_toggle.html', {'obj': obj})
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,3 +1,54 @@
|
||||
from django.test import TestCase
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
# Create your tests here.
|
||||
from vergabe_teilnahme.apps.ausschreibungen.tests import AusschreibungFactory
|
||||
from vergabe_teilnahme.apps.lose.models import Los
|
||||
|
||||
from .models import Subunternehmer, SubunternehmerZuordnung
|
||||
|
||||
|
||||
def make_sub(praeferenz='zugelassen', name='TestSub', **kwargs):
|
||||
return Subunternehmer.objects.create(name=name, praeferenz=praeferenz, **kwargs)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_subunternehmer_zuordnung_zu_los(client):
|
||||
a = AusschreibungFactory()
|
||||
los = Los.objects.create(ausschreibung=a, losnummer='1', lostitel='Los 1')
|
||||
sub = make_sub()
|
||||
url = reverse('partner:su_zuordnen', kwargs={'ausschreibung_id': a.pk, 'los_pk': los.pk})
|
||||
client.post(url, {'subunternehmer_id': sub.pk, 'konkrete_leistung': 'IT-Support'})
|
||||
assert SubunternehmerZuordnung.objects.filter(subunternehmer=sub, ausschreibung=a, los=los).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_gesperrter_subunternehmer_im_suchmodal(client):
|
||||
a = AusschreibungFactory()
|
||||
los = Los.objects.create(ausschreibung=a, losnummer='1', lostitel='Los 1')
|
||||
make_sub(praeferenz='gesperrt', name='GesperrterSub')
|
||||
url = reverse('partner:su_suche_modal', kwargs={'ausschreibung_id': a.pk, 'los_pk': los.pk})
|
||||
response = client.get(url + '?q=GesperrterSub')
|
||||
assert response.status_code == 200
|
||||
assert b'gesperrt' in response.content.lower()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_subunternehmer_praeferenz_update(client):
|
||||
sub = make_sub(praeferenz='zugelassen')
|
||||
url = reverse('partner:su_praeferenz', kwargs={'pk': sub.pk})
|
||||
client.post(url, {'praeferenz': 'bevorzugt'})
|
||||
sub.refresh_from_db()
|
||||
assert sub.praeferenz == 'bevorzugt'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_zuordnung_toggle_zusage(client):
|
||||
a = AusschreibungFactory()
|
||||
los = Los.objects.create(ausschreibung=a, losnummer='1', lostitel='Los 1')
|
||||
sub = make_sub()
|
||||
z = SubunternehmerZuordnung.objects.create(subunternehmer=sub, ausschreibung=a, los=los)
|
||||
assert z.zusage_vorhanden is False
|
||||
url = reverse('partner:zuordnung_toggle', kwargs={'pk': z.pk})
|
||||
client.post(url, {'feld': 'zusage_vorhanden'})
|
||||
z.refresh_from_db()
|
||||
assert z.zusage_vorhanden is True
|
||||
|
||||
@@ -1,2 +1,21 @@
|
||||
from django.urls import path
|
||||
urlpatterns = []
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'partner'
|
||||
|
||||
urlpatterns = [
|
||||
path('subunternehmer/', views.subunternehmer_liste, name='su_liste'),
|
||||
path('subunternehmer/neu/', views.subunternehmer_neu, name='su_neu'),
|
||||
path('subunternehmer/<int:pk>/', views.subunternehmer_detail, name='su_detail'),
|
||||
path('subunternehmer/<int:pk>/bearbeiten/', views.subunternehmer_bearbeiten, name='su_bearbeiten'),
|
||||
path('subunternehmer/<int:pk>/praeferenz/', views.subunternehmer_praeferenz, name='su_praeferenz'),
|
||||
path('subunternehmer/<int:pk>/zuordnung-toggle/', views.zuordnung_toggle, name='zuordnung_toggle'),
|
||||
path('dienstleistertypen/', views.dienstleistertypen_liste, name='dt_liste'),
|
||||
path('dienstleistertypen/neu/', views.dienstleistertyp_neu, name='dt_neu'),
|
||||
path('dienstleistertypen/<int:pk>/bearbeiten/', views.dienstleistertyp_bearbeiten, name='dt_bearbeiten'),
|
||||
path('ausschreibungen/<int:ausschreibung_id>/lose/<int:los_pk>/subunternehmer/suche/',
|
||||
views.subunternehmer_suche_modal, name='su_suche_modal'),
|
||||
path('ausschreibungen/<int:ausschreibung_id>/lose/<int:los_pk>/subunternehmer/zuordnen/',
|
||||
views.subunternehmer_zuordnen, name='su_zuordnen'),
|
||||
]
|
||||
|
||||
@@ -1,3 +1,236 @@
|
||||
from django.shortcuts import render
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
# Create your views here.
|
||||
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||
from vergabe_teilnahme.apps.lose.models import Los
|
||||
|
||||
from .models import Dienstleistertyp, Subunternehmer, SubunternehmerZuordnung
|
||||
|
||||
|
||||
class SubunternehmerForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Subunternehmer
|
||||
fields = [
|
||||
'name', 'kurzname', 'dienstleistertyp', 'praeferenz',
|
||||
'strasse', 'plz', 'ort', 'land',
|
||||
'telefon', 'mobilnummer', 'email', 'website',
|
||||
'ansprechpartner', 'ansprechpartner_email', 'ansprechpartner_telefon',
|
||||
'leistungsprofil', 'bewertung',
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'kurzname': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'dienstleistertyp': forms.Select(attrs={'class': 'form-select'}),
|
||||
'praeferenz': forms.RadioSelect(),
|
||||
'strasse': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'plz': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'ort': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'land': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'telefon': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'mobilnummer': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'email': forms.EmailInput(attrs={'class': 'form-input'}),
|
||||
'website': forms.URLInput(attrs={'class': 'form-input'}),
|
||||
'ansprechpartner': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'ansprechpartner_email': forms.EmailInput(attrs={'class': 'form-input'}),
|
||||
'ansprechpartner_telefon': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'leistungsprofil': forms.Textarea(attrs={'class': 'form-input', 'rows': 4}),
|
||||
'bewertung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
}
|
||||
|
||||
|
||||
class DienstleistertypForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Dienstleistertyp
|
||||
fields = [
|
||||
'name', 'beschreibung', 'typische_leistungen', 'typische_nachweise',
|
||||
'relevante_standards', 'typische_preisbestandteile', 'bemerkungen',
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'typische_leistungen': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'typische_nachweise': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||
'relevante_standards': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||
'typische_preisbestandteile': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||
'bemerkungen': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||
}
|
||||
|
||||
|
||||
def subunternehmer_liste(request):
|
||||
qs = Subunternehmer.objects.select_related('dienstleistertyp')
|
||||
|
||||
q = request.GET.get('q', '').strip()
|
||||
if q:
|
||||
qs = qs.filter(name__icontains=q)
|
||||
|
||||
praeferenz = request.GET.get('praeferenz', '')
|
||||
if praeferenz:
|
||||
qs = qs.filter(praeferenz=praeferenz)
|
||||
|
||||
dt_id = request.GET.get('dienstleistertyp', '')
|
||||
if dt_id:
|
||||
qs = qs.filter(dienstleistertyp_id=dt_id)
|
||||
|
||||
ctx = {
|
||||
'subunternehmer': qs,
|
||||
'dienstleistertypen': Dienstleistertyp.objects.all(),
|
||||
'praeferenz_choices': Subunternehmer.PRAEFERENZ_CHOICES,
|
||||
'q': q,
|
||||
'current_praeferenz': praeferenz,
|
||||
'current_dt': dt_id,
|
||||
'breadcrumbs': [{'label': 'Subunternehmer', 'url': None}],
|
||||
}
|
||||
return render(request, 'partner/subunternehmer_liste.html', ctx)
|
||||
|
||||
|
||||
def subunternehmer_neu(request):
|
||||
if request.method == 'POST':
|
||||
form = SubunternehmerForm(request.POST)
|
||||
if form.is_valid():
|
||||
obj = form.save()
|
||||
messages.success(request, f'Subunternehmer „{obj.name}" angelegt.')
|
||||
return redirect('partner:su_detail', pk=obj.pk)
|
||||
else:
|
||||
form = SubunternehmerForm()
|
||||
|
||||
return render(request, 'partner/subunternehmer_form.html', {
|
||||
'form': form,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Subunternehmer', 'url': '/partner/subunternehmer/'},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def subunternehmer_detail(request, pk):
|
||||
sub = get_object_or_404(Subunternehmer, pk=pk)
|
||||
zuordnungen = SubunternehmerZuordnung.objects.filter(
|
||||
subunternehmer=sub
|
||||
).select_related('ausschreibung', 'los').order_by('-ausschreibung__erstellt_am')
|
||||
|
||||
return render(request, 'partner/subunternehmer_detail.html', {
|
||||
'sub': sub,
|
||||
'zuordnungen': zuordnungen,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Subunternehmer', 'url': '/partner/subunternehmer/'},
|
||||
{'label': sub.name, 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def subunternehmer_bearbeiten(request, pk):
|
||||
sub = get_object_or_404(Subunternehmer, pk=pk)
|
||||
if request.method == 'POST':
|
||||
form = SubunternehmerForm(request.POST, instance=sub)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'Gespeichert.')
|
||||
return redirect('partner:su_detail', pk=pk)
|
||||
else:
|
||||
form = SubunternehmerForm(instance=sub)
|
||||
|
||||
return render(request, 'partner/subunternehmer_form.html', {
|
||||
'form': form,
|
||||
'sub': sub,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Subunternehmer', 'url': '/partner/subunternehmer/'},
|
||||
{'label': sub.name, 'url': f'/partner/subunternehmer/{pk}/'},
|
||||
{'label': 'Bearbeiten', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def subunternehmer_praeferenz(request, pk):
|
||||
sub = get_object_or_404(Subunternehmer, pk=pk)
|
||||
if request.method == 'POST':
|
||||
sub.praeferenz = request.POST.get('praeferenz', sub.praeferenz)
|
||||
if sub.praeferenz == 'gesperrt':
|
||||
begruendung = request.POST.get('begruendung', '').strip()
|
||||
if begruendung:
|
||||
sub.bewertung = begruendung
|
||||
sub.save(update_fields=['praeferenz', 'bewertung'])
|
||||
return render(request, 'partner/partials/praeferenz_badge.html', {'sub': sub})
|
||||
|
||||
|
||||
def subunternehmer_suche_modal(request, ausschreibung_id, los_pk):
|
||||
q = request.GET.get('q', '').strip()
|
||||
subunternehmer = Subunternehmer.objects.filter(name__icontains=q).select_related('dienstleistertyp')
|
||||
return render(request, 'partner/partials/subunternehmer_suche.html', {
|
||||
'subunternehmer': subunternehmer,
|
||||
'los_pk': los_pk,
|
||||
'ausschreibung_id': ausschreibung_id,
|
||||
'q': q,
|
||||
})
|
||||
|
||||
|
||||
def subunternehmer_zuordnen(request, ausschreibung_id, los_pk):
|
||||
if request.method == 'POST':
|
||||
sub_id = request.POST.get('subunternehmer_id')
|
||||
sub = get_object_or_404(Subunternehmer, pk=sub_id)
|
||||
los = get_object_or_404(Los, pk=los_pk)
|
||||
zuordnung, _ = SubunternehmerZuordnung.objects.get_or_create(
|
||||
subunternehmer=sub,
|
||||
ausschreibung_id=ausschreibung_id,
|
||||
los=los,
|
||||
defaults={'konkrete_leistung': request.POST.get('konkrete_leistung', '')},
|
||||
)
|
||||
return render(request, 'partner/partials/zuordnung_zeile.html', {'zuordnung': zuordnung})
|
||||
return redirect('partner:su_liste')
|
||||
|
||||
|
||||
def zuordnung_toggle(request, pk):
|
||||
zuordnung = get_object_or_404(SubunternehmerZuordnung, pk=pk)
|
||||
feld = request.POST.get('feld')
|
||||
if feld in ('zusage_vorhanden', 'nachweis_eingegangen', 'preis_vorhanden'):
|
||||
setattr(zuordnung, feld, not getattr(zuordnung, feld))
|
||||
zuordnung.save(update_fields=[feld])
|
||||
return render(request, 'partner/partials/zuordnung_zeile.html', {'zuordnung': zuordnung})
|
||||
|
||||
|
||||
def dienstleistertypen_liste(request):
|
||||
typen = Dienstleistertyp.objects.all()
|
||||
return render(request, 'partner/dienstleistertyp_liste.html', {
|
||||
'typen': typen,
|
||||
'breadcrumbs': [{'label': 'Dienstleistertypen', 'url': None}],
|
||||
})
|
||||
|
||||
|
||||
def dienstleistertyp_neu(request):
|
||||
if request.method == 'POST':
|
||||
form = DienstleistertypForm(request.POST)
|
||||
if form.is_valid():
|
||||
obj = form.save()
|
||||
messages.success(request, f'Dienstleistertyp „{obj.name}" angelegt.')
|
||||
return redirect('partner:dt_liste')
|
||||
else:
|
||||
form = DienstleistertypForm()
|
||||
|
||||
return render(request, 'partner/dienstleistertyp_form.html', {
|
||||
'form': form,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Dienstleistertypen', 'url': '/partner/dienstleistertypen/'},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def dienstleistertyp_bearbeiten(request, pk):
|
||||
obj = get_object_or_404(Dienstleistertyp, pk=pk)
|
||||
if request.method == 'POST':
|
||||
form = DienstleistertypForm(request.POST, instance=obj)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'Gespeichert.')
|
||||
return redirect('partner:dt_liste')
|
||||
else:
|
||||
form = DienstleistertypForm(instance=obj)
|
||||
|
||||
return render(request, 'partner/dienstleistertyp_form.html', {
|
||||
'form': form,
|
||||
'obj': obj,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Dienstleistertypen', 'url': '/partner/dienstleistertypen/'},
|
||||
{'label': 'Bearbeiten', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{% if obj %}Bearbeiten{% else %}Neue Entscheidungsregel{% endif %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-2xl">
|
||||
<h1 class="page-title mb-6">{% if obj %}{{ obj.regelname }} bearbeiten{% else %}Neue Entscheidungsregel{% endif %}</h1>
|
||||
|
||||
<form method="post" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
<div class="card space-y-4">
|
||||
<div><label class="form-label">Regelname *</label>{{ form.regelname }}</div>
|
||||
<div><label class="form-label">Beschreibung</label>{{ form.beschreibung }}</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div><label class="form-label">Kategorie *</label>{{ form.kategorie }}</div>
|
||||
<div><label class="form-label">Gewichtung</label>{{ form.gewichtung }}</div>
|
||||
</div>
|
||||
<div><label class="form-label">Bewertungslogik</label>{{ form.bewertungslogik }}</div>
|
||||
<div><label class="form-label">Schwellenwert</label>{{ form.schwellenwert }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card space-y-4">
|
||||
<h2 class="section-title">Empfehlung</h2>
|
||||
<div class="flex gap-4">{{ form.empfehlung }}</div>
|
||||
<div><label class="form-label">Begründung</label>{{ form.begruendung }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card space-y-4">
|
||||
<h2 class="section-title">Gültigkeit</h2>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div><label class="form-label">Gültig von</label>{{ form.gueltig_von }}</div>
|
||||
<div><label class="form-label">Gültig bis</label>{{ form.gueltig_bis }}</div>
|
||||
<div><label class="form-label">Verantwortlicher</label>{{ form.verantwortlicher }}</div>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-sm">{{ form.aktiv }} Aktiv</label>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<a href="{% url 'bibliothek:entscheidungsregeln_liste' %}" class="btn-secondary">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Entscheidungsregeln{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h1 class="page-title">Entscheidungsregeln</h1>
|
||||
<a href="{% url 'bibliothek:entscheidungsregel_neu' %}" class="btn-primary">+ Neu</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{% if regeln %}
|
||||
<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">Regelname</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Kategorie</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Empfehlung</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Aktiv</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in regeln %}
|
||||
<tr class="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td class="py-2 font-medium">{{ r.regelname }}</td>
|
||||
<td class="py-2 text-slate-600">{{ r.get_kategorie_display }}</td>
|
||||
<td class="py-2">
|
||||
{% if r.empfehlung == 'teilnehmen' %}<span class="badge-green">Teilnehmen</span>
|
||||
{% elif r.empfehlung == 'nicht_teilnehmen' %}<span class="badge-red">Nicht teilnehmen</span>
|
||||
{% else %}<span class="badge-gray">Prüfen</span>{% endif %}
|
||||
</td>
|
||||
<td class="py-2" id="er-toggle-{{ r.pk }}">
|
||||
<form hx-post="{% url 'bibliothek:er_toggle' pk=r.pk %}"
|
||||
hx-target="#er-toggle-{{ r.pk }}"
|
||||
hx-swap="innerHTML">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="text-sm {% if r.aktiv %}text-green-600{% else %}text-slate-400{% endif %}">
|
||||
{% if r.aktiv %}✓ Aktiv{% else %}✗ Inaktiv{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="py-2 text-right">
|
||||
<a href="{% url 'bibliothek:entscheidungsregel_bearbeiten' pk=r.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 Entscheidungsregeln angelegt.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{% if obj %}Bearbeiten{% else %}Neues Leistungsblatt{% endif %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-3xl">
|
||||
<h1 class="page-title mb-6">{% if obj %}{{ obj.produktfunktion }} bearbeiten{% else %}Neues Leistungsblatt{% endif %}</h1>
|
||||
|
||||
<form method="post" class="card space-y-4">
|
||||
{% csrf_token %}
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="col-span-2"><label class="form-label">Produktfunktion *</label>{{ form.produktfunktion }}</div>
|
||||
<div><label class="form-label">Version</label>{{ form.version }}</div>
|
||||
</div>
|
||||
<div><label class="form-label">Beschreibung</label>{{ form.beschreibung }}</div>
|
||||
<div><label class="form-label">Leistungsumfang</label>{{ form.leistungsumfang }}</div>
|
||||
<div><label class="form-label">Grenzen & Ausschlüsse</label>{{ form.grenzen_ausschluesse }}</div>
|
||||
<div><label class="form-label">Technische Voraussetzungen</label>{{ form.technische_voraussetzungen }}</div>
|
||||
<div><label class="form-label">Typische Nachweise</label>{{ form.typische_nachweise }}</div>
|
||||
<div><label class="form-label">Eigentümer</label>{{ form.eigentuemer }}</div>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<a href="{% url 'bibliothek:leistungsblaetter_liste' %}" class="btn-secondary">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Leistungsblätter{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h1 class="page-title">Leistungsblätter</h1>
|
||||
<a href="{% url 'bibliothek:leistungsblatt_neu' %}" class="btn-primary">+ Neu</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{% if blaetter %}
|
||||
<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">Produktfunktion</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Version</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Eigentümer</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for b in blaetter %}
|
||||
<tr class="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td class="py-2 font-medium">{{ b.produktfunktion }}</td>
|
||||
<td class="py-2 text-slate-600">{{ b.version }}</td>
|
||||
<td class="py-2 text-slate-600">{{ b.eigentuemer|default:"—" }}</td>
|
||||
<td class="py-2 text-right">
|
||||
<a href="{% url 'bibliothek:leistungsblatt_bearbeiten' pk=b.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 Leistungsblätter angelegt.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
45
vergabe_teilnahme/templates/bibliothek/nachweis_detail.html
Normal file
45
vergabe_teilnahme/templates/bibliothek/nachweis_detail.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ nachweis.titel }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex items-start justify-between mb-5">
|
||||
<div>
|
||||
<h1 class="page-title">{{ nachweis.titel }}</h1>
|
||||
<p class="text-slate-500 text-sm">Version {{ nachweis.version }} · {{ nachweis.get_freigabestatus_display }}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="{% url 'bibliothek:nachweis_version' pk=nachweis.pk %}" class="btn-secondary">Neue Version</a>
|
||||
<a href="{% url 'bibliothek:nachweis_bearbeiten' pk=nachweis.pk %}" class="btn-secondary">Bearbeiten</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="card col-span-2 space-y-3">
|
||||
{% if nachweis.kurzbeschreibung %}<p class="text-sm">{{ nachweis.kurzbeschreibung }}</p>{% endif %}
|
||||
{% if nachweis.zugehoerige_standards %}
|
||||
<p class="text-sm"><strong>Standards:</strong> {{ nachweis.zugehoerige_standards }}</p>
|
||||
{% endif %}
|
||||
{% if nachweis.datei %}
|
||||
<p class="text-sm"><a href="{{ nachweis.datei.url }}" class="text-brand-600 hover:underline">Datei herunterladen</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card space-y-2 text-sm">
|
||||
<p><span class="text-slate-500">Gültig ab:</span> {{ nachweis.gueltig_ab|date:"d.m.Y"|default:"—" }}</p>
|
||||
<p><span class="text-slate-500">Gültig bis:</span>
|
||||
{% if nachweis.gueltig_bis %}
|
||||
{% if nachweis.ist_abgelaufen %}<span class="text-red-600 font-medium">{{ nachweis.gueltig_bis|date:"d.m.Y" }} (abgelaufen)</span>
|
||||
{% else %}{{ nachweis.gueltig_bis|date:"d.m.Y" }}{% endif %}
|
||||
{% else %}—{% endif %}
|
||||
</p>
|
||||
<p><span class="text-slate-500">Vertraulichkeit:</span> {{ nachweis.get_vertraulichkeit_display }}</p>
|
||||
<p><span class="text-slate-500">Öffentliche Vergabe:</span> {% if nachweis.fuer_oeffentliche %}Ja{% else %}Nein{% endif %}</p>
|
||||
<p><span class="text-slate-500">Privatwirtschaft:</span> {% if nachweis.fuer_privatwirtschaftliche %}Ja{% else %}Nein{% endif %}</p>
|
||||
{% if nachweis.eigentuemer %}
|
||||
<p><span class="text-slate-500">Eigentümer:</span> {{ nachweis.eigentuemer }}</p>
|
||||
{% endif %}
|
||||
{% if nachweis.letzte_pruefung %}
|
||||
<p><span class="text-slate-500">Letzte Prüfung:</span> {{ nachweis.letzte_pruefung|date:"d.m.Y" }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
58
vergabe_teilnahme/templates/bibliothek/nachweis_form.html
Normal file
58
vergabe_teilnahme/templates/bibliothek/nachweis_form.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{% if neue_version %}Neue Version: {{ nachweis.titel }}{% elif nachweis %}Bearbeiten{% else %}Neuer Nachweis{% endif %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-3xl">
|
||||
<h1 class="page-title mb-6">
|
||||
{% if neue_version %}Neue Version: {{ nachweis.titel }}
|
||||
{% elif nachweis %}{{ nachweis.titel }} bearbeiten
|
||||
{% else %}Neuer Nachweis{% endif %}
|
||||
</h1>
|
||||
|
||||
{% if neue_version %}
|
||||
<div class="bg-amber-50 border border-amber-200 rounded p-3 mb-4 text-sm text-amber-800">
|
||||
Der alte Nachweis (Version {{ nachweis.version }}) wird als „ersetzt" markiert.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
<div class="card space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="col-span-2"><label class="form-label">Titel *</label>{{ form.titel }}</div>
|
||||
</div>
|
||||
<div><label class="form-label">Kurzbeschreibung</label>{{ form.kurzbeschreibung }}</div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div><label class="form-label">Dokumenttyp</label>{{ form.dokumenttyp }}</div>
|
||||
<div><label class="form-label">Kategorie</label>{{ form.kategorie }}</div>
|
||||
<div><label class="form-label">Version</label>{{ form.version }}</div>
|
||||
</div>
|
||||
<div><label class="form-label">Datei</label>{{ form.datei }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card space-y-4">
|
||||
<h2 class="section-title">Gültigkeit & Freigabe</h2>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div><label class="form-label">Gültig ab</label>{{ form.gueltig_ab }}</div>
|
||||
<div><label class="form-label">Gültig bis</label>{{ form.gueltig_bis }}</div>
|
||||
<div><label class="form-label">Letzte Prüfung</label>{{ form.letzte_pruefung }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div><label class="form-label">Freigabestatus</label>{{ form.freigabestatus }}</div>
|
||||
<div><label class="form-label">Vertraulichkeit</label>{{ form.vertraulichkeit }}</div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center gap-2 text-sm">{{ form.fuer_oeffentliche }} Öffentliche Vergabe</label>
|
||||
<label class="flex items-center gap-2 text-sm">{{ form.fuer_privatwirtschaftliche }} Privatwirtschaft</label>
|
||||
</div>
|
||||
<div><label class="form-label">Eigentümer</label>{{ form.eigentuemer }}</div>
|
||||
<div><label class="form-label">Zugehörige Standards</label>{{ form.zugehoerige_standards }}</div>
|
||||
<div><label class="form-label">Sprache</label>{{ form.sprache }}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<a href="{% url 'bibliothek:nachweise_liste' %}" class="btn-secondary">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
62
vergabe_teilnahme/templates/bibliothek/nachweis_liste.html
Normal file
62
vergabe_teilnahme/templates/bibliothek/nachweis_liste.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Nachweise{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h1 class="page-title">Nachweise</h1>
|
||||
<a href="{% url 'bibliothek:nachweis_neu' %}" class="btn-primary">+ Neu</a>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mb-4">
|
||||
<a href="?tab=alle" class="{% if tab == 'alle' %}btn-primary{% else %}btn-secondary{% endif %} btn-sm">Alle</a>
|
||||
<a href="?tab=bald_ablaufend" class="{% if tab == 'bald_ablaufend' %}btn-primary{% else %}btn-secondary{% endif %} btn-sm">Bald ablaufend</a>
|
||||
<a href="?tab=abgelaufen" class="{% if tab == 'abgelaufen' %}btn-primary{% else %}btn-secondary{% endif %} btn-sm">Abgelaufen</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{% if nachweise %}
|
||||
<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">Titel</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Version</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Gültig bis</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Freigabe</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for n in nachweise %}
|
||||
<tr class="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td class="py-2 font-medium">
|
||||
<a href="{% url 'bibliothek:nachweis_detail' pk=n.pk %}" class="text-brand-600 hover:underline">{{ n.titel }}</a>
|
||||
</td>
|
||||
<td class="py-2 text-slate-600">{{ n.version }}</td>
|
||||
<td class="py-2">
|
||||
{% if n.gueltig_bis %}
|
||||
{% if n.ist_abgelaufen %}
|
||||
<span class="badge-red">{{ n.gueltig_bis|date:"d.m.Y" }}</span>
|
||||
{% elif n.gueltig_bis <= in_60_tagen %}
|
||||
<span class="badge-orange">{{ n.gueltig_bis|date:"d.m.Y" }}</span>
|
||||
{% else %}
|
||||
<span class="text-slate-600">{{ n.gueltig_bis|date:"d.m.Y" }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-slate-400">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="py-2">
|
||||
<span class="badge-gray">{{ n.get_freigabestatus_display }}</span>
|
||||
</td>
|
||||
<td class="py-2 text-right">
|
||||
<a href="{% url 'bibliothek:nachweis_bearbeiten' pk=n.pk %}" class="text-slate-400 hover:text-slate-700 text-xs mr-2">Bearbeiten</a>
|
||||
<a href="{% url 'bibliothek:nachweis_version' pk=n.pk %}" class="text-slate-400 hover:text-slate-700 text-xs">Neue Version</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-slate-500 text-sm">Keine Nachweise gefunden.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
<form hx-post="{% url 'bibliothek:er_toggle' pk=obj.pk %}"
|
||||
hx-target="closest td"
|
||||
hx-swap="innerHTML">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="text-sm {% if obj.aktiv %}text-green-600{% else %}text-slate-400{% endif %}">
|
||||
{% if obj.aktiv %}✓ Aktiv{% else %}✗ Inaktiv{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
@@ -0,0 +1,33 @@
|
||||
<div>
|
||||
<div class="mb-3">
|
||||
<input type="text" name="q" value="{{ q }}" class="form-input w-full"
|
||||
placeholder="Referenz suchen (Titel, Branche)..."
|
||||
hx-get="{% url 'bibliothek:referenz_zuordnen' ausschreibung_id=ausschreibung.pk %}"
|
||||
hx-target="closest div"
|
||||
hx-trigger="input changed delay:300ms"
|
||||
hx-include="[name='q']">
|
||||
</div>
|
||||
|
||||
{% if referenzen %}
|
||||
<ul class="space-y-1 max-h-80 overflow-y-auto">
|
||||
{% for ref in referenzen %}
|
||||
<li class="flex items-start justify-between p-2 rounded border border-slate-100 hover:bg-slate-50">
|
||||
<div>
|
||||
<p class="font-medium text-sm">{{ ref.referenztitel|truncatechars:60 }}</p>
|
||||
<p class="text-xs text-slate-500">{{ ref.kunde }} · {{ ref.branche|default:"" }}</p>
|
||||
{% if ref.freigabestatus_verwendung == 'eingeschraenkt' %}
|
||||
<p class="text-xs text-amber-700">⚠ Eingeschränkte Verwendung: {{ ref.einschraenkungen_verwendung|truncatechars:60 }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<form method="post" action="{% url 'bibliothek:referenz_zuordnen' ausschreibung_id=ausschreibung.pk %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="referenz_id" value="{{ ref.pk }}">
|
||||
<button type="submit" class="btn-primary btn-sm">Zuordnen</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-slate-500 text-sm">Keine Referenzen gefunden.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
67
vergabe_teilnahme/templates/bibliothek/referenz_detail.html
Normal file
67
vergabe_teilnahme/templates/bibliothek/referenz_detail.html
Normal file
@@ -0,0 +1,67 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ ref.referenztitel }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex items-start justify-between mb-5">
|
||||
<div>
|
||||
<h1 class="page-title">{{ ref.referenztitel }}</h1>
|
||||
<p class="text-slate-500 text-sm">{{ ref.kunde }}{% if ref.branche %} · {{ ref.branche }}{% endif %}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{% if ref.whitepaper %}
|
||||
<a href="{{ ref.whitepaper.url }}" class="btn-secondary" download>Whitepaper</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'bibliothek:referenz_bearbeiten' pk=ref.pk %}" class="btn-secondary">Bearbeiten</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="card col-span-2 space-y-4">
|
||||
{% if ref.kurzfassung %}
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-slate-700 mb-1">Kurzfassung</h3>
|
||||
<p class="text-sm text-slate-700">{{ ref.kurzfassung }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if ref.leistungsbeschreibung %}
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-slate-700 mb-1">Leistungsbeschreibung</h3>
|
||||
<p class="text-sm text-slate-700">{{ ref.leistungsbeschreibung }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if ref.eingesetzte_produkte %}
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-slate-700 mb-1">Eingesetzte Produkte</h3>
|
||||
<p class="text-sm text-slate-700">{{ ref.eingesetzte_produkte }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if ref.leistungsblaetter.exists %}
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-slate-700 mb-1">Leistungsblätter</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for lb in ref.leistungsblaetter.all %}
|
||||
<span class="badge-gray">{{ lb.produktfunktion }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card space-y-2 text-sm">
|
||||
<p><span class="text-slate-500">Projektzeitraum:</span> {{ ref.projektzeitraum|default:"—" }}</p>
|
||||
<p><span class="text-slate-500">Art:</span> {{ ref.oeffentlich_oder_privat|default:"—" }}</p>
|
||||
{% if ref.vertragsvolumen %}
|
||||
<p><span class="text-slate-500">Volumen:</span> {{ ref.vertragsvolumen|floatformat:0 }} €</p>
|
||||
{% endif %}
|
||||
<p><span class="text-slate-500">Freigabe:</span> {{ ref.get_freigabestatus_verwendung_display }}</p>
|
||||
{% if not ref.verwendbar_fuer_ausschreibungen %}
|
||||
<p class="text-red-600">Nicht für Ausschreibungen verwendbar</p>
|
||||
{% endif %}
|
||||
{% if ref.einschraenkungen_verwendung %}
|
||||
<p class="text-amber-700 text-xs">⚠ {{ ref.einschraenkungen_verwendung }}</p>
|
||||
{% endif %}
|
||||
{% if ref.ansprechpartner_referenzkunde %}
|
||||
<p><span class="text-slate-500">Ansprechpartner:</span> {{ ref.ansprechpartner_referenzkunde }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
52
vergabe_teilnahme/templates/bibliothek/referenz_form.html
Normal file
52
vergabe_teilnahme/templates/bibliothek/referenz_form.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{% if ref %}{{ ref.referenztitel }} bearbeiten{% else %}Neue Referenz{% endif %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-3xl">
|
||||
<h1 class="page-title mb-6">{% if ref %}Bearbeiten{% else %}Neue Referenz{% endif %}</h1>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
<div class="card space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="col-span-2"><label class="form-label">Referenztitel *</label>{{ form.referenztitel }}</div>
|
||||
<div><label class="form-label">Kunde *</label>{{ form.kunde }}</div>
|
||||
<div><label class="form-label">Branche</label>{{ form.branche }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div><label class="form-label">Art (öffentlich/privat)</label>{{ form.oeffentlich_oder_privat }}</div>
|
||||
<div><label class="form-label">Projektzeitraum</label>{{ form.projektzeitraum }}</div>
|
||||
<div><label class="form-label">Vertragsvolumen (€)</label>{{ form.vertragsvolumen }}</div>
|
||||
</div>
|
||||
<div><label class="form-label">Leistungsbeschreibung</label>{{ form.leistungsbeschreibung }}</div>
|
||||
<div><label class="form-label">Eingesetzte Produkte</label>{{ form.eingesetzte_produkte }}</div>
|
||||
<div><label class="form-label">Ansprechpartner Referenzkunde</label>{{ form.ansprechpartner_referenzkunde }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card space-y-4">
|
||||
<h2 class="section-title">Freigabe & Verwendung</h2>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div><label class="form-label">Freigabestatus</label>{{ form.freigabestatus_verwendung }}</div>
|
||||
<div><label class="form-label">Vertraulichkeit</label>{{ form.vertraulichkeit }}</div>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-sm">{{ form.verwendbar_fuer_ausschreibungen }} Verwendbar für Ausschreibungen</label>
|
||||
<div><label class="form-label">Einschränkungen</label>{{ form.einschraenkungen_verwendung }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card space-y-4">
|
||||
<h2 class="section-title">Inhalte & Dokumente</h2>
|
||||
<div><label class="form-label">Kurzfassung</label>{{ form.kurzfassung }}</div>
|
||||
<div><label class="form-label">Langfassung</label>{{ form.langfassung }}</div>
|
||||
<div><label class="form-label">Whitepaper</label>{{ form.whitepaper }}</div>
|
||||
<div>
|
||||
<label class="form-label">Leistungsblätter</label>
|
||||
<div class="mt-1 space-y-1">{{ form.leistungsblaetter }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<a href="{% url 'bibliothek:referenz_liste' %}" class="btn-secondary">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
59
vergabe_teilnahme/templates/bibliothek/referenz_liste.html
Normal file
59
vergabe_teilnahme/templates/bibliothek/referenz_liste.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Referenzen{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h1 class="page-title">Referenzen</h1>
|
||||
<a href="{% url 'bibliothek:referenz_neu' %}" class="btn-primary">+ Neu</a>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<form method="get" class="flex gap-3 items-end">
|
||||
<div class="flex-1">
|
||||
<label class="form-label">Suche (Titel, Branche, Leistung)</label>
|
||||
<input type="text" name="q" value="{{ q }}" class="form-input w-full" placeholder="Suchen...">
|
||||
</div>
|
||||
<button type="submit" class="btn-secondary">Suchen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{% if referenzen %}
|
||||
<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">Referenz</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Kunde</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Branche</th>
|
||||
<th class="pb-2 font-medium text-slate-600">Freigabe</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ref in referenzen %}
|
||||
<tr class="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td class="py-2 font-medium">
|
||||
<a href="{% url 'bibliothek:referenz_detail' pk=ref.pk %}" class="text-brand-600 hover:underline">{{ ref.referenztitel|truncatechars:60 }}</a>
|
||||
</td>
|
||||
<td class="py-2 text-slate-600">{{ ref.kunde }}</td>
|
||||
<td class="py-2 text-slate-600">{{ ref.branche|default:"—" }}</td>
|
||||
<td class="py-2">
|
||||
{% if ref.freigabestatus_verwendung == 'freigegeben' %}
|
||||
<span class="badge-green">Freigegeben</span>
|
||||
{% elif ref.freigabestatus_verwendung == 'eingeschraenkt' %}
|
||||
<span class="badge-orange">Eingeschränkt</span>
|
||||
{% else %}
|
||||
<span class="badge-red">Nicht freigegeben</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="py-2 text-right">
|
||||
<a href="{% url 'bibliothek:referenz_bearbeiten' pk=ref.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 Referenzen gefunden.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user