diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..214637d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,36 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**Vergabe Teilnahme** is a web-based tender/bid management system (internal collaboration tool) that supports a company through the full lifecycle of public and private procurement bids — from initial research through post-award retrospective. The language of the application and all domain documentation is **German**. + +This repo is currently **pre-implementation**. The authoritative requirements are in `wiki/ProductRequirementsDocument.md`. + +## Planned Tech Stack + +The .gitignore targets **Python** (Django, Flask, uv, Ruff, pytest). No framework or tooling has been selected yet — check for a `pyproject.toml`, `Pipfile`, or `requirements.txt` before assuming. + +## Domain Model — Key Concepts + +The system manages **Ausschreibungen** (tenders) through 8 phases: + +| Phase | Name | +|-------|------| +| 1 | Recherche & Unterlagen bereitstellen | +| 2 | Teilnahmeentscheidung treffen | +| 3 | Detaillierte Durchsicht & offene Punkte | +| 4 | Bieterfragen, Subunternehmer, offene Punkte klären | +| 5 | Preismodell dokumentieren | +| 6 | Unterlagen finalisieren | +| 7 | Abgabe | +| 8 | Zuschlag / Nachbetrachtung | + +Core entities: `Ausschreibung`, `Los` (lot), `Anforderung` (requirement), `Aufgabe` (task), `Bieterfrage` (bidder question), `Dokument`, `Subunternehmer`, `Preispunkt` (price point), `Marktbegleiter` (competitor), `Nachweis` (compliance certificate), `Referenz`, `Freigabe` (approval), `Nachbetrachtung` (retrospective). + +**Vergleichsgewicht** (comparison weight): price points carry a weight in [0.0, 2.0] (default 1.0). Weighted averages use `Σ(value × weight) / Σ(weight)`; points with weight 0.0 are excluded from averages entirely. + +## Custodian State Hub + +This repo is tracked by the Custodian State Hub. At session start inside this repo, call `get_domain_summary("")` via the `state-hub` MCP tool. End every non-trivial session with `add_progress_event()`. diff --git a/wiki/ArchitectureBlueprint.md b/wiki/ArchitectureBlueprint.md new file mode 100644 index 0000000..e76256f --- /dev/null +++ b/wiki/ArchitectureBlueprint.md @@ -0,0 +1,892 @@ +# Architecture Blueprint — Vergabe Teilnahme + +**Version:** 1.0 +**Datum:** 7. Mai 2026 +**Grundlage:** ProductRequirementsDocument.md + +--- + +## 1. Leitprinzipien + +| Prinzip | Konsequenz | +|---|---| +| **Phasengeführt, nie phasengesperrt** | Die 8 Phasen sind Navigation und Orientierung, keine Zugangssperren. Jedes Element ist jederzeit erreichbar. | +| **Kein Zwang zur Vollständigkeit** | Pflichtfelder gibt es nur an echten Systemgrenzen (z. B. Abgabenachweis). Alle anderen Felder können leer bleiben. | +| **Anpassbare Datenstruktur** | Jede Entität erlaubt das globale Ausblenden ungenutzter Felder und das Hinzufügen beliebiger Schlüssel-Wert-Attribute. | +| **Manuelle Erfassung first** | Keine Automatisierung in v1. Alle Felder werden manuell gepflegt. | +| **Server-first, progressive Enhancement** | Seitenrendering auf dem Server. HTMX für partielle Aktualisierungen. Alpine.js für reine UI-Zustände. | + +--- + +## 2. Technology Stack + +| Schicht | Technologie | Begründung | +|---|---|---| +| Sprache | Python 3.12+ | .gitignore-Konvention; erprobtes Ökosystem | +| Paketmanager | uv | Geschwindigkeit, lockfile, Python-Versions-Management | +| Web-Framework | Django 5.x | ORM, Admin, Auth, ContentType-Framework — alle benötigt | +| Template-Rendering | Django Templates | Server-side, kein Build-Step für Templates | +| Reaktivität | HTMX 2.x | Partielle DOM-Updates ohne SPA-Overhead | +| UI-Zustandsverwaltung | Alpine.js 3.x | Accordions, Dropdowns, Modals, Inline-Toggles | +| CSS | Tailwind CSS 4.x | Utility-first; Design-Token-System (s. Abschnitt 8) | +| CSS-Build | PostCSS + Vite | Tailwind JIT, Minification | +| Datenbank | PostgreSQL 16+ | JSONB, volltextsuche, referentielle Integrität | +| Datei-Storage | Django FileField (lokal) | Einfach; abstrakt genug für S3 via django-storages later | +| Background-Tasks | Celery + Redis | Fristenprüfung, Benachrichtigungen (optional in v1) | +| Testing | pytest-django | Fixture-basiert; kein Mock der DB | + +--- + +## 3. Django-App-Struktur + +Das Projekt ist in fachliche Apps aufgeteilt. Technische Querschnittsthemen liegen in `core`. + +``` +vergabe_teilnahme/ # Django-Projekt-Root +├── core/ # Querschnittslogik +│ ├── models.py # FlexibleModel-Mixin, CustomAttribute, EntityFieldConfig, Freigabe +│ ├── mixins.py # FlexFieldMixin für Views +│ ├── templatetags/ # render_field, flex_fields, phase_badge, status_badge +│ └── services.py # weighted_average(), deadline_warnings() +├── ausschreibungen/ # Ausschreibung, Teilnahmeentscheidung +├── lose/ # Los, Anforderung, Auflage +├── aufgaben/ # Aufgabe (Offener Punkt), Bieterfrage +├── dokumente/ # Dokument, Datei-Upload, Versionierung +├── preise/ # Preispunkt, Preismodell, Auswertungen +├── partner/ # Subunternehmer, Dienstleistertyp +├── bibliothek/ # Nachweis, Referenz, Leistungsblatt, Entscheidungsregel +├── marktbegleiter/ # Marktbegleiter, Ausschreibungspassage +├── nachbetrachtung/ # Nachbetrachtung (Phase 8) +├── feedback/ # Feedbackeintrag +└── accounts/ # Mitarbeiter (erweitertes Django-User-Modell) +``` + +### App-Abhängigkeiten (vereinfacht) + +``` +core ←── alle Apps +accounts ←── ausschreibungen, lose, aufgaben, dokumente, preise, partner +ausschreibungen ←── lose, aufgaben, dokumente, preise, marktbegleiter, nachbetrachtung +bibliothek ←── lose (Anforderungs-Nachweise), dokumente (Standarddokumente) +partner ←── lose, aufgaben, preise +``` + +--- + +## 4. Flexible Felder — Das Anpassungssystem + +Jede Entität unterstützt zwei Anpassungsmechanismen: + +### 4.1 Felder ausblenden: `EntityFieldConfig` + +Definiert **global** (nicht pro Instanz), welche eingebauten Felder einer Entität ausgeblendet werden. Einmal konfiguriert — wirkt für alle Instanzen dieses Typs. + +```python +# core/models.py + +class EntityFieldConfig(Model): + entity_type = CharField(max_length=100) # z. B. "subunternehmer", "anforderung" + field_name = CharField(max_length=100) # interner Feldname + is_hidden = BooleanField(default=False) + display_label = CharField(max_length=200, blank=True) # Umbenennungsoption + sort_order = PositiveSmallIntegerField(default=0) + + class Meta: + unique_together = ('entity_type', 'field_name') +``` + +**Template-Tag** `{% render_field obj "mobilnummer" %}` prüft `EntityFieldConfig` und rendert das Feld nur, wenn `is_hidden=False`. + +**Verwaltungsoberfläche:** Unter `/admin/felder/` können Administratoren für jeden Entitätstyp in einer Tabelle alle Felder sehen, ein-/ausblenden und umbenennen. Keine Code-Änderung erforderlich. + +### 4.2 Benutzerdefinierte Schlüssel-Wert-Attribute: `CustomAttribute` + +Für jede Instanz beliebig erweiterbar. Verwendet Django's ContentType-Framework (Generic FK). + +```python +# core/models.py + +class CustomAttribute(Model): + DATA_TYPES = [ + ('text', 'Text'), + ('number', 'Zahl'), + ('date', 'Datum'), + ('boolean', 'Ja / Nein'), + ('url', 'Link'), + ('email', 'E-Mail'), + ] + + content_type = ForeignKey(ContentType, on_delete=CASCADE) + object_id = PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') + + key = CharField(max_length=100) # interner Schlüssel (slug) + label = CharField(max_length=200) # Anzeigebezeichnung + value = TextField(blank=True) + data_type = CharField(max_length=20, choices=DATA_TYPES, default='text') + sort_order = PositiveSmallIntegerField(default=0) + created_at = DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['sort_order', 'created_at'] + indexes = [Index(fields=['content_type', 'object_id'])] +``` + +**UX:** Jede Detailseite zeigt am Ende einen Abschnitt "Weitere Attribute" mit einem "+ Attribut hinzufügen"-Button (HTMX-Inline-Formular). Bestehende Attribute können bearbeitet, umsortiert und gelöscht werden. + +### 4.3 Mixin für alle Entitäts-Modelle + +```python +# core/models.py + +class FlexibleModel(Model): + """Abstract base für alle Fachentitäten.""" + + custom_attributes = GenericRelation(CustomAttribute) + + def get_visible_fields(self): + hidden = EntityFieldConfig.objects.filter( + entity_type=self._meta.model_name, + is_hidden=True + ).values_list('field_name', flat=True) + return [f for f in self._meta.get_fields() if f.name not in hidden] + + class Meta: + abstract = True +``` + +--- + +## 5. Datenmodelle + +### 5.1 Ausschreibung + +```python +class Ausschreibung(FlexibleModel): + STATUS_CHOICES = [ + (1, 'Recherchiert / angelegt'), + (2, 'In Erstprüfung'), + (3, 'Teilnahme entschieden'), + (4, 'Detailanalyse läuft'), + (5, 'Klärung / Bieterfragen läuft'), + (6, 'Preisgestaltung läuft'), + (7, 'Unterlagenfinalisierung läuft'), + (8, 'Bereit zur Abgabe'), + (9, 'Abgegeben'), + (10, 'Zuschlag gewonnen'), + (11, 'Zuschlag verloren'), + (12, 'Aufgehoben / zurückgezogen'), + (13, 'Archiviert'), + ] + TEILNAHME_CHOICES = [ + ('offen', 'Offen'), + ('teilnahme', 'Teilnahme'), + ('ablehnung', 'Nichtteilnahme'), + ('pruefung', 'Weitere Prüfung erforderlich'), + ] + + titel = CharField(max_length=400) + ausschreiber = CharField(max_length=300) + plattform = CharField(max_length=200, blank=True) + plattform_link = URLField(blank=True) + ansprechpartner = CharField(max_length=300, blank=True) + hauptverantwortung = ForeignKey('accounts.Mitarbeiter', null=True, blank=True, + on_delete=SET_NULL, related_name='verantwortete_ausschreibungen') + status = PositiveSmallIntegerField(choices=STATUS_CHOICES, default=1) + teilnahmeentscheidung = CharField(max_length=20, choices=TEILNAHME_CHOICES, default='offen') + beschreibung = TextField(blank=True) + strategische_relevanz = TextField(blank=True) + ergebnis = CharField(max_length=20, blank=True) # gewonnen, verloren, aufgehoben, offen + archiviert = BooleanField(default=False) + + # Fristen + bieterfragen_bis = DateField(null=True, blank=True) + abgabe_bis = DateTimeField(null=True, blank=True) + zuschlag_bis = DateField(null=True, blank=True) + produktiv_bis = DateField(null=True, blank=True) + + erstellt_am = DateTimeField(auto_now_add=True) + geaendert_am = DateTimeField(auto_now=True) +``` + +### 5.2 Los + +```python +class Los(FlexibleModel): + ausschreibung = ForeignKey(Ausschreibung, on_delete=CASCADE, related_name='lose') + losnummer = CharField(max_length=50) + lostitel = CharField(max_length=300) + beschreibung = TextField(blank=True) + abgrenzung = TextField(blank=True) + zustaendiger = ForeignKey('accounts.Mitarbeiter', null=True, blank=True, on_delete=SET_NULL) + teilnahme = BooleanField(null=True) # None = noch nicht entschieden + status = CharField(max_length=50, blank=True) + + class Meta: + ordering = ['losnummer'] +``` + +### 5.3 Anforderung / Auflage + +```python +class Anforderung(FlexibleModel): + VERBINDLICHKEIT = [('muss', 'Muss'), ('soll', 'Soll'), ('kann', 'Kann')] + ERFUELLUNGSSTATUS = [ + ('offen', 'Offen'), + ('in_pruefung', 'In Prüfung'), + ('erfuellbar', 'Erfüllbar'), + ('mit_subunternehmer', 'Erfüllbar mit Subunternehmer'), + ('nicht_erfuellbar', 'Nicht erfüllbar'), + ('klaerung', 'Klärung erforderlich'), + ] + + ausschreibung = ForeignKey(Ausschreibung, on_delete=CASCADE) + los = ForeignKey(Los, null=True, blank=True, on_delete=SET_NULL) + titel = CharField(max_length=400) + beschreibung = TextField(blank=True) + quelle_im_dokument = CharField(max_length=300, blank=True) + kategorie = CharField(max_length=100, blank=True) + verbindlichkeit = CharField(max_length=10, choices=VERBINDLICHKEIT, default='muss') + ausschlusskriterium = BooleanField(default=False) + bewertungskriterium = BooleanField(default=False) + zustaendiger = ForeignKey('accounts.Mitarbeiter', null=True, blank=True, on_delete=SET_NULL) + erfuellungsstatus = CharField(max_length=30, choices=ERFUELLUNGSSTATUS, default='offen') + nachweis_erforderlich = BooleanField(default=False) + dokumente = ManyToManyField('dokumente.Dokument', blank=True) + nachweise = ManyToManyField('bibliothek.Nachweis', blank=True) +``` + +### 5.4 Aufgabe (Offener Punkt) + +```python +class Aufgabe(FlexibleModel): + TYP_CHOICES = [ + ('fachlich', 'Fachlich'), + ('rechtlich', 'Rechtlich'), + ('kaufmaennisch', 'Kaufmännisch'), + ('technisch', 'Technisch'), + ('subunternehmer', 'Subunternehmerklärung'), + ('dokument', 'Dokumentenaufgabe'), + ('preis', 'Preisaufgabe'), + ] + STATUS_CHOICES = [ + ('offen', 'Offen'), + ('in_bearbeitung', 'In Bearbeitung'), + ('wartend_intern', 'Wartend auf intern'), + ('wartend_sub', 'Wartend auf Subunternehmer'), + ('wartend_ausschreiber', 'Wartend auf Ausschreiber'), + ('erledigt', 'Erledigt'), + ('verworfen', 'Verworfen'), + ('ueberfaellig', 'Überfällig'), + ] + + ausschreibung = ForeignKey(Ausschreibung, on_delete=CASCADE) + los = ForeignKey(Los, null=True, blank=True, on_delete=SET_NULL) + anforderung = ForeignKey(Anforderung, null=True, blank=True, on_delete=SET_NULL) + bieterfrage = ForeignKey('aufgaben.Bieterfrage', null=True, blank=True, on_delete=SET_NULL) + dokument = ForeignKey('dokumente.Dokument', null=True, blank=True, on_delete=SET_NULL) + + titel = CharField(max_length=400) + beschreibung = TextField(blank=True) + typ = CharField(max_length=20, choices=TYP_CHOICES) + prioritaet = PositiveSmallIntegerField(default=2) # 1=hoch, 2=mittel, 3=niedrig + frist = DateField(null=True, blank=True) + verantwortlicher = ForeignKey('accounts.Mitarbeiter', null=True, blank=True, on_delete=SET_NULL) + status = CharField(max_length=30, choices=STATUS_CHOICES, default='offen') + ergebnis = TextField(blank=True) +``` + +### 5.5 Bieterfrage + +```python +class Bieterfrage(FlexibleModel): + STATUS_CHOICES = [ + ('entwurf', 'Entwurf'), + ('abgestimmt', 'Intern abgestimmt'), + ('eingereicht','Eingereicht'), + ('beantwortet','Beantwortet'), + ('eingearbeitet', 'Eingearbeitet'), + ] + + ausschreibung = ForeignKey(Ausschreibung, on_delete=CASCADE) + anforderung = ForeignKey(Anforderung, null=True, blank=True, on_delete=SET_NULL) + dokument = ForeignKey('dokumente.Dokument', null=True, blank=True, on_delete=SET_NULL) + frage = TextField() + hintergrund = TextField(blank=True) + verantwortlicher = ForeignKey('accounts.Mitarbeiter', null=True, blank=True, on_delete=SET_NULL) + status = CharField(max_length=20, choices=STATUS_CHOICES, default='entwurf') + prioritaet = PositiveSmallIntegerField(default=2) + einreichungsdatum = DateField(null=True, blank=True) + antwort = TextField(blank=True) + auswirkung_angebot = TextField(blank=True) + eingearbeitet = BooleanField(default=False) +``` + +### 5.6 Dokument + +```python +class Dokument(FlexibleModel): + STATUS_CHOICES = [ + ('hochgeladen', 'Hochgeladen'), + ('zu_pruefen', 'Zu prüfen'), + ('in_bearbeitung', 'In Bearbeitung'), + ('geprueft', 'Geprüft'), + ('freigegeben', 'Freigegeben'), + ('final_abgegeben','Final abgegeben'), + ('ersetzt', 'Ersetzt / veraltet'), + ('archiviert', 'Archiviert'), + ] + KATEGORIE_CHOICES = [ + ('leistungsverzeichnis', 'Leistungsverzeichnis'), + ('vertragsunterlagen', 'Vertragsunterlagen'), + ('preisblatt', 'Preisblatt'), + ('formblatt', 'Formblatt'), + ('eignungsnachweis', 'Eignungsnachweis'), + ('technische_anlage', 'Technische Anlage'), + ('bieterinformation', 'Bieterinformation'), + ('sonstiges', 'Sonstiges'), + ] + + ausschreibung = ForeignKey(Ausschreibung, null=True, blank=True, on_delete=SET_NULL) + los = ForeignKey(Los, null=True, blank=True, on_delete=SET_NULL) + datei = FileField(upload_to='dokumente/%Y/%m/') + dateiname = CharField(max_length=300) + kategorie = CharField(max_length=50, choices=KATEGORIE_CHOICES) + version = CharField(max_length=50, default='1.0') + quelle = CharField(max_length=200, blank=True) + status = CharField(max_length=20, choices=STATUS_CHOICES, default='hochgeladen') + verantwortlicher = ForeignKey('accounts.Mitarbeiter', null=True, blank=True, + on_delete=SET_NULL, related_name='verantwortete_dokumente') + pruefer = ForeignKey('accounts.Mitarbeiter', null=True, blank=True, + on_delete=SET_NULL, related_name='zu_pruefende_dokumente') + finale_abgabeversion = BooleanField(default=False) + upload_datum = DateTimeField(auto_now_add=True) +``` + +### 5.7 Preispunkt + +```python +from decimal import Decimal + +class Preispunkt(FlexibleModel): + ausschreibung = ForeignKey(Ausschreibung, on_delete=CASCADE) + los = ForeignKey(Los, null=True, blank=True, on_delete=SET_NULL) + leistungstyp = CharField(max_length=200) + konkrete_leistung = CharField(max_length=400) + mengeneinheit = CharField(max_length=50, blank=True) + menge = DecimalField(max_digits=14, decimal_places=4, null=True, blank=True) + einzelpreis = DecimalField(max_digits=14, decimal_places=2, null=True, blank=True) + gesamtpreis = DecimalField(max_digits=14, decimal_places=2, null=True, blank=True) + waehrung = CharField(max_length=3, default='EUR') + preisstand = DateField(null=True, blank=True) + wiederkehrend = BooleanField(default=False) + laufzeitbezug = CharField(max_length=100, blank=True) + subunternehmeranteil = BooleanField(default=False) + subunternehmer = ForeignKey('partner.Subunternehmer', null=True, blank=True, on_delete=SET_NULL) + + # Vergleichsgewicht: 0.0 – 2.0, Default 1.0 + vergleichsgewicht = DecimalField(max_digits=3, decimal_places=1, default=Decimal('1.0')) + gewichtungsbegruendung = TextField(blank=True) + kommentar = TextField(blank=True) + + # Kontextmarkierungen für Auswertungen + ausschreibung_gewonnen = BooleanField(null=True) +``` + +**Gewichteter Durchschnitt** (Service-Funktion in `preise/services.py`): + +```python +from decimal import Decimal + +def gewichteter_durchschnitt(preispunkte, feld='einzelpreis'): + relevante = [p for p in preispunkte + if getattr(p, feld) is not None and p.vergleichsgewicht > 0] + summe_gewichte = sum(p.vergleichsgewicht for p in relevante) + if summe_gewichte == 0: + return None + summe = sum(getattr(p, feld) * p.vergleichsgewicht for p in relevante) + return { + 'wert': summe / summe_gewichte, + 'summe_gewichte': summe_gewichte, + 'anzahl': len(relevante), + 'minimum': min(getattr(p, feld) for p in relevante), + 'maximum': max(getattr(p, feld) for p in relevante), + } +``` + +### 5.8 Freigabe (generic) + +```python +class Freigabe(Model): + TYP_CHOICES = [ + ('teilnahme', 'Teilnahmefreigabe'), + ('ablehnung', 'Nichtteilnahmefreigabe'), + ('recht', 'Rechts-/Compliance-Freigabe'), + ('preis', 'Preisfreigabe'), + ('abgabe', 'Finale Abgabefreigabe'), + ('standarddokument','Freigabe Standarddokument'), + ('referenz', 'Freigabe Referenzunterlage'), + ] + STATUS_CHOICES = [ + ('erteilt', 'Erteilt'), + ('abgelehnt', 'Abgelehnt'), + ('ausstehend', 'Ausstehend'), + ] + + content_type = ForeignKey(ContentType, on_delete=CASCADE) + object_id = PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') + + freigabe_typ = CharField(max_length=30, choices=TYP_CHOICES) + freigebende_person = ForeignKey(settings.AUTH_USER_MODEL, on_delete=PROTECT) + status = CharField(max_length=20, choices=STATUS_CHOICES, default='erteilt') + kommentar = TextField(blank=True) + timestamp = DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-timestamp'] +``` + +### 5.9 Subunternehmer + +```python +class Subunternehmer(FlexibleModel): + PRAEFERENZ = [('bevorzugt', 'Bevorzugt'), ('zugelassen', 'Zugelassen'), ('gesperrt', 'Gesperrt')] + + name = CharField(max_length=300) + dienstleistertyp = ForeignKey('partner.Dienstleistertyp', null=True, blank=True, on_delete=SET_NULL) + leistungsbereiche = TextField(blank=True) + ansprechpartner = CharField(max_length=200, blank=True) + email = EmailField(blank=True) + mobilnummer = CharField(max_length=50, blank=True) + adresse = TextField(blank=True) + webseite = URLField(blank=True) + zertifizierungen = TextField(blank=True) + praeferenz = CharField(max_length=20, choices=PRAEFERENZ, default='zugelassen') + bewertung = TextField(blank=True) + typische_preislogik = TextField(blank=True) + bemerkungen = TextField(blank=True) +``` + +### 5.10 Weitere Entitäten (Kurzform) + +| Entität | App | Besonderheiten | +|---|---|---| +| `Dienstleistertyp` | `partner` | Katalog; kein FK auf Ausschreibung | +| `Nachweis` | `bibliothek` | `gueltig_ab`, `gueltig_bis` → Ablaufwarnung | +| `Referenz` | `bibliothek` | `whitepaper` FileField; Freigabestatus via `Freigabe` | +| `Leistungsblatt` | `bibliothek` | Versioniert; zugehörige Referenzen M2M | +| `Entscheidungsregel` | `bibliothek` | Kein Automat; liefert Empfehlung + Score | +| `Marktbegleiter` | `marktbegleiter` | Globaler Katalog | +| `Ausschreibungspassage` | `marktbegleiter` | FK auf `Ausschreibung`, `Dokument`, `Marktbegleiter`; Verlässlichkeitsscore 0–10 | +| `Nachbetrachtung` | `nachbetrachtung` | 1:1 mit Ausschreibung; enthält Verlustgründe als JSON-Array | +| `Mitarbeiter` | `accounts` | Erweitert `AbstractUser`; Rolle, Org-Einheit | +| `Feedbackeintrag` | `feedback` | Seite/Kontext als CharField; optional FK auf Ausschreibung | + +--- + +## 6. Navigationsarchitektur + +### 6.1 Shell-Layout + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ TOPBAR │ +│ [Logo] [Globale Suche ] [🔔] [Avatar ▾] │ +├──────────────┬───────────────────────────────────────────────────┤ +│ │ BREADCRUMB │ +│ SIDEBAR ├───────────────────────────────────────────────────┤ +│ (240 px) │ │ +│ │ HAUPTINHALT │ +│ Globale Nav │ │ +│ │ │ +│ ───────── │ │ +│ │ │ +│ Phasen-Nav │ │ +│ (wenn in │ │ +│ Ausschrei- │ │ +│ bung) │ │ +│ │ │ +└──────────────┴───────────────────────────────────────────────────┘ + [💬 Feedback-Button] +``` + +Der Feedback-Button ist auf jeder Seite unten rechts persistent sichtbar und öffnet ein HTMX-Modal. + +### 6.2 Sidebar — Globale Abschnitte + +``` +▸ Übersicht (Dashboard) +▸ Ausschreibungen + └─ + Neue Ausschreibung + +▸ Bibliothek + ├─ Dokumente + ├─ Nachweise & Compliance + ├─ Referenzen + ├─ Leistungsblätter + └─ Entscheidungsregeln + +▸ Partner + ├─ Subunternehmer + └─ Dienstleistertypen + +▸ Marktbegleiter + +▸ Feedback-Backlog + +▸ Administration + └─ Felder & Stammdaten ← EntityFieldConfig-Verwaltung +``` + +### 6.3 Sidebar — Phasen-Navigator (kontextuell) + +Erscheint unterhalb der globalen Nav, wenn der Nutzer in einer Ausschreibung navigiert: + +``` +▾ [Ausschreibung: Titel] + ┌─ ① Recherche & Unterlagen (aktuell: Phase 4) + ├─ ② Teilnahmeentscheidung ✓ + ├─ ③ Detailanalyse ● ← aktive Phase hervorgehoben + ├─ ④ Bieterfragen & Klärung ● + ├─ ⑤ Preisgestaltung + ├─ ⑥ Unterlagen finalisieren + ├─ ⑦ Abgabe + └─ ⑧ Nachbetrachtung +``` + +Jede Phasennummer ist ein direkter Link. Die Phasenzahl im Status-Feld der Ausschreibung steuert nur die visuelle Hervorhebung — sie sperrt keinen Zugang. + +**Phasenindikator-Zustände:** + +| Symbol | Bedeutung | +|---|---| +| `○` | Noch nicht begonnen | +| `●` | In Bearbeitung | +| `✓` | Abgeschlossen | +| `⚠` | Aktion erforderlich (überfällige Aufgaben o. Ä.) | + +### 6.4 Dashboard + +Das Dashboard zeigt aggregierte Kacheln und Listen: + +- **Kritische Fristen** — Ausschreibungen mit `abgabe_bis` ≤ 14 Tage +- **Ohne Entscheidung** — Status 1–2, älter als 3 Tage +- **Überfällige Aufgaben** — Aufgaben mit `frist < heute` und Status ≠ erledigt/verworfen +- **Laufende Ausschreibungen** — Status 3–8 +- **Gewonnen / Verloren** — Letzte 30 Tage +- **Ablaufende Nachweise** — Bibliothek-Nachweise mit `gueltig_bis` ≤ 60 Tage + +--- + +## 7. URL-Struktur + +``` +/ # Dashboard + +/ausschreibungen/ # Liste + Filter +/ausschreibungen/neu/ # Erstellen +/ausschreibungen// # Detail / Phase 1 (Stammdaten + Dokumente) +/ausschreibungen//entscheidung/ # Phase 2 (Teilnahmeentscheidung) +/ausschreibungen//lose/ # Phase 3a (Lose) +/ausschreibungen//lose// # Los-Detail +/ausschreibungen//anforderungen/ # Phase 3b (Anforderungen) +/ausschreibungen//aufgaben/ # Phase 3c + 4 (Aufgaben) +/ausschreibungen//bieterfragen/ # Phase 4 (Bieterfragen) +/ausschreibungen//preise/ # Phase 5 (Preispunkte + Auswertung) +/ausschreibungen//abgabe/ # Phase 6 + 7 (Checkliste + Nachweis) +/ausschreibungen//nachbetrachtung/ # Phase 8 (Ergebnis + Lessons Learned) +/ausschreibungen//marktbegleiter/ # Passagen dieser Ausschreibung +/ausschreibungen//freigaben/ # Alle Freigaben dieser Ausschreibung + +/bibliothek/dokumente/ # Standarddokumentbibliothek +/bibliothek/nachweise/ # Compliance-/QM-Nachweise +/bibliothek/nachweise// # Nachweis-Detail +/bibliothek/referenzen/ # Referenzdatenbank +/bibliothek/referenzen// # Referenz-Detail +/bibliothek/leistungsblaetter/ # Leistungsblätter +/bibliothek/entscheidungsregeln/ # Entscheidungsregelkatalog + +/partner/subunternehmer/ # Subunternehmer-Katalog +/partner/subunternehmer// # Subunternehmer-Detail +/partner/dienstleistertypen/ # Dienstleistertypen + +/marktbegleiter/ # Marktbegleiter-Liste +/marktbegleiter// # Profil + Passagen + +/feedback/ # Feedback-Backlog (Admin-Sicht) + +/admin/felder/ # EntityFieldConfig-Verwaltung +/admin/nutzer/ # Mitarbeiterverwaltung +``` + +--- + +## 8. Tailwind Design System + +### 8.1 Konfiguration (`tailwind.config.js`) + +```js +export default { + content: ['./templates/**/*.html', './*/templates/**/*.html'], + theme: { + extend: { + colors: { + brand: { + 50: '#f0f4ff', + 100: '#dce7ff', + 500: '#3b5bdb', // Primär + 600: '#2f4ac7', + 700: '#2541b2', + 900: '#152d99', + }, + phase: { + todo: '#94a3b8', // slate-400 + active: '#3b82f6', // blue-500 + done: '#22c55e', // green-500 + warning: '#f59e0b', // amber-500 + }, + }, + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + }, + }, + }, +} +``` + +### 8.2 Status-Badges + +Alle Status-Badges werden via Template-Tag `{% status_badge value entity_type %}` gerendert. Die Farben: + +| Kategorie | Farbe | +|---|---| +| offen / ausstehend | `bg-slate-100 text-slate-700` | +| in Bearbeitung | `bg-blue-100 text-blue-700` | +| erledigt / gewonnen / freigegeben | `bg-green-100 text-green-700` | +| überfällig / nicht erfüllbar / verloren | `bg-red-100 text-red-700` | +| wartend / weitere Prüfung | `bg-amber-100 text-amber-700` | +| archiviert / ersetzt | `bg-gray-100 text-gray-500` | + +### 8.3 Komponenten-Klassen (`@layer components`) + +```css +@layer components { + .card { @apply bg-white rounded-xl border border-slate-200 shadow-sm p-6; } + .btn-primary { @apply bg-brand-500 text-white px-4 py-2 rounded-lg hover:bg-brand-600 transition-colors; } + .btn-secondary { @apply bg-white text-slate-700 border border-slate-300 px-4 py-2 rounded-lg hover:bg-slate-50; } + .btn-danger { @apply bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700; } + .btn-ghost { @apply text-slate-600 px-3 py-2 rounded-lg hover:bg-slate-100; } + + .field-row { @apply grid grid-cols-3 gap-4 py-3 border-b border-slate-100 last:border-0; } + .field-label { @apply text-sm font-medium text-slate-500 col-span-1; } + .field-value { @apply text-sm text-slate-900 col-span-2; } + + .phase-badge { @apply inline-flex items-center justify-center w-7 h-7 rounded-full text-sm font-bold; } + .phase-todo { @apply phase-badge bg-slate-200 text-slate-500; } + .phase-active { @apply phase-badge bg-brand-500 text-white; } + .phase-done { @apply phase-badge bg-green-500 text-white; } + .phase-warn { @apply phase-badge bg-amber-400 text-amber-900; } + + .section-title { @apply text-base font-semibold text-slate-900 mb-4; } + .page-title { @apply text-2xl font-bold text-slate-900; } + + .form-input { @apply w-full rounded-lg border border-slate-300 px-3 py-2 text-sm + focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent; } + .form-label { @apply block text-sm font-medium text-slate-700 mb-1; } + + .table-base { @apply w-full text-sm text-left; } + .table-header { @apply bg-slate-50 text-slate-500 font-medium text-xs uppercase tracking-wide; } + .table-row { @apply border-t border-slate-100 hover:bg-slate-50 transition-colors; } +} +``` + +### 8.4 Typographie + +- Überschriften: `font-bold` + entsprechende `text-*` Größen (H1 = `text-2xl`, H2 = `text-xl`, H3 = `text-base`) +- Beschreibungstexte: `text-sm text-slate-600` +- Pflicht-Markierung: roter Stern `*` via Template-Tag + +--- + +## 9. HTMX-Interaktionsmuster + +### 9.1 Inline-Bearbeitung + +Detailfelder rendern als `` mit einem Edit-Icon. Klick → HTMX GET `/…/edit/?field=titel` → Inline-Formular ersetzt den ``. Speichern → HTMX POST → aktualisierter `` wird zurückgegeben. + +```html +
+ {{ obj.titel }} + +
+``` + +### 9.2 Listen mit Lazy-Loading + +Lange Listen (Aufgaben, Anforderungen, Preispunkte) laden initial nur die ersten 25 Einträge. Ein "Mehr laden"-Button am Ende nutzt `hx-get` + `hx-swap="beforeend"` für paginiertes Nachladen. + +### 9.3 Status-Wechsel ohne Reload + +Status-Dropdowns in Listenzeilen nutzen `hx-post` + `hx-target="closest tr"` um nur die Zeile neu zu rendern. + +### 9.4 Custom-Attribute-Panel + +Jede Detailseite endet mit: + +```html +
+ Lade... +
+``` + +Hinzufügen: Button öffnet Inline-Formular (HTMX). Speichern: Neues Attribut erscheint sofort in der Liste. + +### 9.5 Feedback-Modal + +```html + + + +``` + +Das Modal schreibt `seite={{ request.path }}` und `ausschreibung={{ current_ausschreibung.id|default:'' }}` als Hidden-Felder automatisch in den Feedbackeintrag. + +--- + +## 10. Alpine.js UI-Zustände + +Alpine.js (keine Server-Kommunikation) für: + +- **Sidebar Collapse:** `x-data="{ open: true }" @click="open = !open"` +- **Phasen-Accordion:** Aufklappen einzelner Phasen-Abschnitte im Phasen-Navigator +- **Bestätigungsdialoge:** `x-show="confirm"` bei Löschen-Aktionen +- **Dropdown-Menüs:** Kontextmenüs in Listentabellen +- **Feld-Ausblenden im Formular:** Checkbox "Feld ausblenden" toggelt via Alpine das zugehörige Input sofort + +--- + +## 11. Dokument-Storage + +### Verzeichnisstruktur + +``` +media/ +└── dokumente/ + └── / + ├── leistungsverzeichnis/ + ├── vertragsunterlagen/ + ├── preisblatt/ + └── ... +``` + +### Upload-Validierung + +Erlaubte MIME-Typen: `application/pdf`, `application/msword`, `application/vnd.openxmlformats-officedocument.*`, `application/vnd.ms-excel`, `application/zip`, `image/png`, `image/jpeg`. + +Maximale Dateigröße: 50 MB (konfigurierbar via `settings.MAX_UPLOAD_SIZE`). + +### Versionierung + +Neue Version = neues `Dokument`-Objekt. Das alte erhält `status='ersetzt'`. Beide bleiben in der DB und im Dateisystem erhalten. Die Detailansicht zeigt eine Versionshistorie-Liste. + +### Produktions-Abstraktion + +`DEFAULT_FILE_STORAGE` in `settings.py` wechseln auf `storages.backends.s3boto3.S3Boto3Storage` — kein Code in den Apps muss geändert werden. + +--- + +## 12. Phasen-Zustandsmaschine + +Der Status der Ausschreibung ist ein Integer 1–13. Übergänge sind nicht erzwungen. Ein Utility gibt den "empfohlenen nächsten Status" zurück: + +```python +# ausschreibungen/services.py + +NAECHSTER_STATUS = { + 1: 2, # Angelegt → In Erstprüfung + 2: 3, # Erstprüfung → Entscheidung + 3: 4, # Entschieden → Detailanalyse + 4: 5, # Detailanalyse → Bieterfragen + 5: 6, # Bieterfragen → Preisgestaltung + 6: 7, # Preisgestaltung → Unterlagenfinalisierung + 7: 8, # Finalisierung → Bereit zur Abgabe + 8: 9, # Bereit → Abgegeben + 9: 10, # Abgegeben → (manuell: 10 gewonnen / 11 verloren / 12 aufgehoben) +} + +ABSCHLUSS_STATUS = {10, 11, 12, 13} + +def naechster_empfohlener_status(ausschreibung): + return NAECHSTER_STATUS.get(ausschreibung.status) +``` + +--- + +## 13. Sucharchitektur + +Für v1: Django ORM `icontains`-Suche über Titel, Ausschreiber, Beschreibung. Globale Suchleiste gibt einen unified result set zurück: + +```python +# search/views.py + +def global_search(request): + q = request.GET.get('q', '') + return { + 'ausschreibungen': Ausschreibung.objects.filter(titel__icontains=q)[:5], + 'aufgaben': Aufgabe.objects.filter(titel__icontains=q)[:5], + 'dokumente': Dokument.objects.filter(dateiname__icontains=q)[:5], + 'subunternehmer': Subunternehmer.objects.filter(name__icontains=q)[:5], + 'marktbegleiter': Marktbegleiter.objects.filter(name__icontains=q)[:5], + } +``` + +PostgreSQL-Volltext (`SearchVector`) kann später ohne URL-Änderung eingebaut werden. + +--- + +## 14. Fristenüberwachung + +`core/services.py` stellt `get_deadline_warnings(ausschreibung)` bereit. In v1 wird dies bei jedem Seitenaufruf der Ausschreibungsdetailseite berechnet (kein Background-Job erforderlich): + +```python +def get_deadline_warnings(ausschreibung): + warnings = [] + heute = date.today() + if ausschreibung.bieterfragen_bis: + delta = (ausschreibung.bieterfragen_bis - heute).days + if delta <= 3: + warnings.append({'typ': 'bieterfragen', 'tage': delta}) + if ausschreibung.abgabe_bis: + delta = (ausschreibung.abgabe_bis.date() - heute).days + if delta <= 14: + warnings.append({'typ': 'abgabe', 'tage': delta}) + for aufgabe in ausschreibung.aufgabe_set.filter(status__in=['offen', 'in_bearbeitung']): + if aufgabe.frist and aufgabe.frist < heute: + warnings.append({'typ': 'aufgabe', 'aufgabe': aufgabe}) + return warnings +``` + +--- + +## 15. Erweiterungspunkte (spätere Ausbaustufen) + +| Bereich | Erweiterungsweg | +|---|---| +| Volltext-Dokumentenanalyse | Apache Tika / pdfplumber auf Upload-Signal; extrahierter Text in `Dokument.volltext` speichern | +| KI-Assistenz | Claude API via Celery-Task; gibt strukturierten JSON-Vorschlag zurück; Nutzer bestätigt manuell | +| E-Mail-Benachrichtigungen | Django Signals + Celery; Template pro Benachrichtigungstyp | +| SharePoint-Integration | `bibliothek`-App kann externe Dokument-URLs speichern (bereits als `quelle`-Feld angelegt) | +| Partnerportal | Separates Django-Projekt mit beschränkten API-Endpoints; Authentifizierung via Token | +| Mehrsprachigkeit | Django i18n; alle Template-Strings in `{% trans %}` | +| Mandantenfähigkeit | `Ausschreibung` bekommt FK auf `Mandant`; alle Querys filtern auf `request.user.mandant` | +| SSO / SAML | `django-allauth` oder `python-saml`; kein Auth-Code in den Fachapps | diff --git a/wiki/UseCaseCatalog.md b/wiki/UseCaseCatalog.md new file mode 100644 index 0000000..5de43ea --- /dev/null +++ b/wiki/UseCaseCatalog.md @@ -0,0 +1,1053 @@ +# Use-Case-Katalog — Vergabe Teilnahme + +**Version:** 1.0 +**Datum:** 8. Mai 2026 +**Grundlage:** ProductRequirementsDocument.md, ArchitectureBlueprint.md + +--- + +## Lesehinweise + +Jeder Use Case beschreibt eine in der UI leicht ausführbare Aktion aus Nutzersicht. Die **Phase** gibt an, in welchem Prozessabschnitt der Use Case typischerweise auftritt — er ist aber durch die freie Navigation jederzeit aufrufbar. Der **Einstiegspunkt** nennt den schnellsten Weg zur relevanten UI-Seite. + +### Akteursrollen (Kurzbezeichnungen) + +| Kürzel | Rolle | +|---|---| +| BM | Bid Manager / Ausschreibungsmanager | +| FV | Fachverantwortliche / Leistungsexperte | +| VT | Vertrieb / Account Management | +| PC | Pricing / Controlling | +| RC | Recht / Compliance | +| GF | Geschäftsführung / Freigabeinstanz | +| PL | Projektleitung Umsetzung | +| AD | Administrator | +| Alle | Jede angemeldete Person | + +### Nummerierung + +`UC-[Bereich]-[Nummer]` — Bereiche: **OV** Überblick · **AS** Ausschreibung · **LA** Lose & Anforderungen · **AU** Aufgaben · **BF** Bieterfragen · **DO** Dokumente · **PR** Preise · **AB** Abgabe · **NB** Nachbetrachtung · **SU** Subunternehmer & Partner · **BIB** Bibliothek · **MB** Marktbegleiter · **FR** Freigaben · **FF** Flexible Felder · **FB** Feedback + +--- + +## A — Überblick & Dashboard + +--- + +### UC-OV-01 — Tagesübersicht aufrufen + +| | | +|---|---| +| **Akteur** | Alle | +| **Einstieg** | Startseite / Logo-Klick | + +1. Nutzer öffnet die Anwendung oder klickt auf das Logo. +2. Dashboard zeigt Kacheln: kritische Fristen, Ausschreibungen ohne Entscheidung, überfällige Aufgaben, ablaufende Nachweise, zuletzt aktive Ausschreibungen. +3. Nutzer klickt eine Kachel an — gelangt direkt zur gefilterten Liste oder zur betreffenden Ausschreibung. + +**Ergebnis:** Nutzer hat einen sofortigen Überblick über dringende Handlungsfelder und navigiert gezielt weiter. + +--- + +### UC-OV-02 — Kritische Abgabefristen überwachen + +| | | +|---|---| +| **Akteur** | BM, GF | +| **Einstieg** | Dashboard → Kachel „Kritische Fristen" | + +1. Dashboard zeigt alle Ausschreibungen mit `abgabe_bis` ≤ 14 Tage, sortiert aufsteigend nach Fristdatum. +2. Jede Zeile zeigt Restlaufzeit in Tagen, Ausschreibungstitel, Verantwortlichen und aktuellen Status. +3. BM klickt auf eine Ausschreibung und gelangt direkt zur Abgabe-Seite (Phase 7). + +**Ergebnis:** Keine Abgabefrist geht im Tagesgeschäft unter. + +--- + +### UC-OV-03 — Ausschreibungsübergreifende Aufgabenliste sehen + +| | | +|---|---| +| **Akteur** | BM, FV | +| **Einstieg** | Dashboard → Kachel „Überfällige Aufgaben" oder Sidebar → Aufgaben (global) | + +1. Nutzer öffnet die globale Aufgabenliste. +2. Liste zeigt alle eigenen Aufgaben aus allen Ausschreibungen, filterbar nach Status, Typ und Fälligkeitsdatum. +3. Nutzer setzt Filter „Nur meine Aufgaben" und „Status: offen + überfällig". +4. Klick auf eine Aufgabe öffnet die zugehörige Ausschreibungsseite an der richtigen Stelle. + +**Ergebnis:** Nutzer erkennt seinen vollständigen Arbeitsvorrat ohne in einzelne Ausschreibungen navigieren zu müssen. + +--- + +## B — Ausschreibungsverwaltung + +--- + +### UC-AS-01 — Neue Ausschreibung anlegen + +| | | +|---|---| +| **Akteur** | BM | +| **Phase** | 1 | +| **Einstieg** | Dashboard → „+ Neue Ausschreibung" oder Sidebar → Ausschreibungen → „+ Neu" | + +1. BM klickt „+ Neue Ausschreibung". +2. Formular öffnet sich mit Pflichtfeld *Titel* und optionalen Stammdatenfeldern (Ausschreiber, Plattform, Fristen, Ansprechpartner). +3. BM trägt mindestens Titel und Ausschreiber ein, setzt Abgabefrist. +4. BM weist sich selbst oder eine andere Person als Hauptverantwortlichen zu. +5. Speichern → Ausschreibung erhält Status „Recherchiert / angelegt". +6. System öffnet direkt die Detailseite der neuen Ausschreibung. + +**Ergebnis:** Ausschreibung ist erfasst und bereit für Dokument-Upload und Bewertung. + +--- + +### UC-AS-02 — Ausschreibung suchen und filtern + +| | | +|---|---| +| **Akteur** | Alle | +| **Einstieg** | Globale Suchleiste (Topbar) oder Sidebar → Ausschreibungen | + +1. Nutzer tippt Suchbegriff in die globale Suchleiste — Ergebnisse erscheinen sofort (HTMX, kein Reload). +2. Alternativ: Nutzer öffnet die Ausschreibungsliste und setzt Filter (Status, Verantwortlicher, Abgabezeitraum, Archiviert ja/nein). +3. Klick auf einen Treffer öffnet die Ausschreibungsdetailseite. + +**Ergebnis:** Nutzer findet jede Ausschreibung in unter drei Schritten — unabhängig davon, in welcher Phase sie sich befindet. + +--- + +### UC-AS-03 — Ausschreibungsstatus manuell aktualisieren + +| | | +|---|---| +| **Akteur** | BM | +| **Einstieg** | Ausschreibung → Stammdaten-Seite → Statusfeld | + +1. BM öffnet die Ausschreibungsdetailseite. +2. Im Statusfeld klickt BM auf den aktuellen Status — ein Dropdown erscheint mit allen 13 Statuswerten. +3. BM wählt den neuen Status; ein optionales Kommentarfeld erscheint. +4. Speichern → Status wird gesetzt, Phasen-Navigator in der Sidebar aktualisiert sich. + +**Ergebnis:** Status spiegelt den tatsächlichen Bearbeitungsstand wider; alle Beteiligten sehen den aktuellen Stand. + +--- + +### UC-AS-04 — Teilnahmeentscheidung dokumentieren + +| | | +|---|---| +| **Akteur** | BM, VT, GF | +| **Phase** | 2 | +| **Einstieg** | Ausschreibung → Phase 2 (Teilnahmeentscheidung) | + +1. BM öffnet die Seite „Teilnahmeentscheidung" der Ausschreibung. +2. Entscheidungskriterien-Liste (aus Entscheidungsregeln-Katalog) wird vorausgefüllt angezeigt; BM bewertet jedes Kriterium. +3. BM wählt Entscheidung: Teilnahme / Nichtteilnahme / Weitere Prüfung. +4. BM trägt Begründung ein, erfasst Risiken und Ausschlussgründe im Freitextfeld. +5. Freigabe wird dokumentiert (→ UC-FR-01). + +**Ergebnis:** Entscheidung ist mit Begründung und Freigabe revisionssicher gespeichert. + +--- + +### UC-AS-05 — Entscheidungsregel für eine Ausschreibung anwenden + +| | | +|---|---| +| **Akteur** | BM, VT | +| **Phase** | 2 | +| **Einstieg** | Ausschreibung → Phase 2 → „Regeln anwenden" | + +1. BM klickt „Regeln anwenden" auf der Entscheidungsseite. +2. System zeigt alle aktiven Entscheidungsregeln mit ihrer Empfehlung (teilnehmen / nicht teilnehmen / prüfen) basierend auf den bisher eingetragenen Daten. +3. Regeln, bei denen kritische Daten fehlen (z. B. fehlende Referenz für Referenz-Pflichtanforderung), werden als Warnung markiert. +4. BM nutzt das Ergebnis als Eingabe für seine Entscheidung — keine automatische Übernahme. + +**Ergebnis:** Teilnahmeentscheidung ist durch strukturierte Regelanwendung nachvollziehbar begründet. + +--- + +### UC-AS-06 — Historische Ausschreibung nacherfassen + +| | | +|---|---| +| **Akteur** | BM, AD | +| **Einstieg** | Sidebar → Ausschreibungen → „+ Neu" → Modus „Historisch erfassen" | + +1. BM legt eine neue Ausschreibung an und aktiviert die Option „Historische Erfassung". +2. Alle Felder inklusive Ergebnis, Verlustgründe und Preispunkte sind sofort befüllbar — ohne Phasenreihenfolge. +3. BM trägt Stammdaten, Dokumente, Ergebnis und Lessons Learned ein. +4. Speichern → Ausschreibung erhält Status entsprechend des eingetragenen Ergebnisses. + +**Ergebnis:** Historische Daten stehen für Preisvergleiche, Marktbegleiteranalyse und Lessons Learned zur Verfügung. + +--- + +### UC-AS-07 — Ausschreibung archivieren + +| | | +|---|---| +| **Akteur** | BM | +| **Einstieg** | Ausschreibung → Stammdaten → „Archivieren" | + +1. BM öffnet die Ausschreibungsdetailseite. +2. BM klickt „Archivieren" im Aktionsmenü. +3. Bestätigungsdialog erscheint. +4. Nach Bestätigung: Status wird auf „Archiviert" gesetzt; Ausschreibung verschwindet aus der Standardliste. +5. In der Ausschreibungsliste ist ein Filter „Archiviert anzeigen" verfügbar, um auf die Ausschreibung zurückzugreifen. + +**Ergebnis:** Abgeschlossene Ausschreibungen belasten die aktive Liste nicht, bleiben aber vollständig abrufbar. + +--- + +## C — Lose & Anforderungen + +--- + +### UC-LA-01 — Los anlegen und abgrenzen + +| | | +|---|---| +| **Akteur** | BM, FV | +| **Phase** | 3 | +| **Einstieg** | Ausschreibung → Phase 3 → „Lose" → „+ Los" | + +1. BM klickt „+ Los" in der Lose-Übersicht. +2. Formular öffnet sich: Losnummer, Lostitel, Beschreibung, Abgrenzung, Zuständiger. +3. BM trägt Losnummer und Titel ein, definiert Abgrenzung (Freitext). +4. BM weist das Los einem Fachverantwortlichen zu. +5. Speichern → Los erscheint in der Lose-Liste; Phasen-Navigator zeigt das Los als Strukturelement. + +**Ergebnis:** Losstruktur ist erfasst und kann für Anforderungen, Aufgaben und Preise als Bezug genutzt werden. + +--- + +### UC-LA-02 — Anforderung erfassen und klassifizieren + +| | | +|---|---| +| **Akteur** | FV, RC | +| **Phase** | 3 | +| **Einstieg** | Ausschreibung → Phase 3 → „Anforderungen" → „+ Anforderung" | + +1. FV klickt „+ Anforderung". +2. Formular: Titel, Beschreibung, Bezugsdokument / Fundstelle, Los (optional), Kategorie, Verbindlichkeit (Muss/Soll/Kann). +3. FV markiert ggf.: Ausschlusskriterium, Bewertungskriterium, Nachweis erforderlich. +4. FV weist der Anforderung eine verantwortliche Person zu. +5. Speichern → Anforderung erscheint in der nach Los gruppierten Liste mit Erfüllungsstatus „Offen". + +**Ergebnis:** Anforderung ist klassifiziert und kann gezielt bearbeitet, verknüpft und verfolgt werden. + +--- + +### UC-LA-03 — Erfüllungsstatus einer Anforderung aktualisieren + +| | | +|---|---| +| **Akteur** | FV, RC | +| **Einstieg** | Ausschreibung → Anforderungen → Anforderung anklicken | + +1. FV öffnet die Anforderungsdetailseite oder klickt den Status-Badge direkt in der Listentabelle. +2. Dropdown erscheint: Offen / In Prüfung / Erfüllbar / Erfüllbar mit Subunternehmer / Nicht erfüllbar / Klärung erforderlich. +3. FV wählt den neuen Status — optional mit Kommentar. +4. Speichern → Status aktualisiert sich inline (kein Seitenreload). + +**Ergebnis:** Der Bearbeitungsstand einer Anforderung ist für alle sichtbar; nicht erfüllbare Anforderungen sind sofort erkennbar. + +--- + +### UC-LA-04 — Anforderung mit Nachweis aus der Bibliothek verknüpfen + +| | | +|---|---| +| **Akteur** | RC, FV | +| **Phase** | 3, 6 | +| **Einstieg** | Anforderungsdetail → Abschnitt „Nachweise" | + +1. RC öffnet eine Anforderung, die als Nachweis erforderlich markiert ist. +2. Im Abschnitt „Nachweise" klickt RC auf „Nachweis zuordnen". +3. Eine Suche öffnet sich, gefiltert auf den Nachweis-Katalog (Bibliothek). +4. RC wählt den passenden Nachweis; Ablaufdatum und Freigabestatus werden direkt angezeigt. +5. Speichern → Nachweis ist mit der Anforderung verknüpft; abgelaufene Nachweise werden mit Warnung markiert. + +**Ergebnis:** Jede Nachweispflicht ist sofort mit dem konkreten, versionierten Nachweis-Dokument verbunden. + +--- + +### UC-LA-05 — Ausschlusskriterium erkennen und eskalieren + +| | | +|---|---| +| **Akteur** | FV, BM | +| **Phase** | 3 | +| **Einstieg** | Ausschreibung → Anforderungen | + +1. FV erfasst eine Anforderung und markiert sie als Ausschlusskriterium (Checkbox). +2. FV setzt Erfüllungsstatus auf „Nicht erfüllbar". +3. System hebt die Anforderung in der Liste rot hervor und zeigt eine Warnung auf der Teilnahmeentscheidungs-Seite (Phase 2). +4. BM sieht die Warnung bei der nächsten Sichtung der Phase-2-Seite und trifft eine informierte Entscheidung. + +**Ergebnis:** Kritische Ausschlussgründe werden sofort sichtbar gemacht, ohne dass BM alle Anforderungen einzeln prüfen muss. + +--- + +## D — Aufgaben + +--- + +### UC-AU-01 — Aufgabe manuell erstellen und zuweisen + +| | | +|---|---| +| **Akteur** | BM, FV | +| **Phase** | 3, 4 | +| **Einstieg** | Ausschreibung → Aufgaben → „+ Aufgabe" | + +1. BM klickt „+ Aufgabe". +2. Formular: Titel, Beschreibung, Typ (fachlich / rechtlich / kaufmännisch / technisch / Subunternehmer / Dokument / Preis), Priorität, Frist. +3. BM weist die Aufgabe einer Person zu. +4. Optional: BM verknüpft die Aufgabe mit einem Los, einer Anforderung oder einer Bieterfrage. +5. Speichern → Aufgabe erscheint in der Liste mit Status „Offen"; verantwortliche Person sieht sie in ihrer globalen Aufgabenliste. + +**Ergebnis:** Aufgabe ist delegiert und nachverfolgbar. + +--- + +### UC-AU-02 — Aufgabe aus Anforderung ableiten + +| | | +|---|---| +| **Akteur** | FV, BM | +| **Phase** | 3 | +| **Einstieg** | Anforderungsdetail → „Aufgabe erstellen" | + +1. FV öffnet eine Anforderung mit Status „Klärung erforderlich" oder „In Prüfung". +2. FV klickt „Aufgabe erstellen" — Formular wird vorausgefüllt mit Anforderungstitel als Aufgabentitel und der Verknüpfung zur Anforderung. +3. FV ergänzt Priorität, Frist und Verantwortlichen. +4. Speichern → Aufgabe ist mit der Anforderung verknüpft; in der Anforderungsdetailseite erscheint die Aufgabe im Abschnitt „Verbundene Aufgaben". + +**Ergebnis:** Anforderung und zugehörige Aufgabe sind rückverfolgbar verknüpft; kein Informationsverlust beim Kontextwechsel. + +--- + +### UC-AU-03 — Aufgabenstatus und Ergebnis dokumentieren + +| | | +|---|---| +| **Akteur** | Alle (Aufgabeninhaber) | +| **Einstieg** | Aufgabenliste → Aufgabe öffnen | + +1. Nutzer öffnet die Aufgabe aus der Ausschreibungs-Aufgabenliste oder der globalen Aufgabenliste. +2. Nutzer ändert Status auf „In Bearbeitung", „Wartend auf Subunternehmer" o. Ä. +3. Nach Erledigung: Status auf „Erledigt" setzen, Ergebnis / Lösung im Ergebnisfeld eintragen. +4. Speichern → Status aktualisiert sich; in der verknüpften Anforderung oder Bieterfrage erscheint die Aufgabe als erledigt. + +**Ergebnis:** Bearbeitungsstand ist dokumentiert; Ergebnis ist dauerhaft abrufbar und nicht nur in E-Mails oder Köpfen. + +--- + +### UC-AU-04 — Überfällige Aufgaben einer Ausschreibung bereinigen + +| | | +|---|---| +| **Akteur** | BM | +| **Einstieg** | Ausschreibung → Aufgaben → Filter „Überfällig" | + +1. BM öffnet die Aufgabenliste der Ausschreibung und setzt Filter „Status: Überfällig". +2. Liste zeigt alle Aufgaben, deren Frist abgelaufen ist und die noch nicht erledigt sind. +3. BM bewertet jede Aufgabe: Frist verlängern, Verantwortlichen wechseln, Aufgabe verwerfen oder als erledigt markieren. +4. Jede Statusänderung geschieht inline ohne Seitenreload. + +**Ergebnis:** Rückstau bei überfälligen Aufgaben wird transparent gemacht und abgebaut. + +--- + +## E — Bieterfragen + +--- + +### UC-BF-01 — Bieterfrage aus einer Anforderung erstellen + +| | | +|---|---| +| **Akteur** | BM, FV | +| **Phase** | 4 | +| **Einstieg** | Anforderungsdetail → „Bieterfrage erstellen" oder Ausschreibung → Bieterfragen → „+ Bieterfrage" | + +1. FV öffnet eine Anforderung mit Unklarheit (Status „Klärung erforderlich"). +2. FV klickt „Bieterfrage erstellen" — Formular öffnet sich mit vorausgefülltem Bezug zur Anforderung. +3. FV formuliert die Frage und ergänzt Hintergrund, Priorität und Verantwortlichen. +4. Speichern → Bieterfrage erhält Status „Entwurf" und erscheint in der Bieterfragen-Liste. + +**Ergebnis:** Bieterfrage ist mit ihrer Herkunft (Anforderung) verknüpft und kann gezielt verfolgt werden. + +--- + +### UC-BF-02 — Bieterfragen abstimmen und als eingereicht markieren + +| | | +|---|---| +| **Akteur** | BM | +| **Phase** | 4 | +| **Einstieg** | Ausschreibung → Bieterfragen | + +1. BM öffnet die Bieterfragen-Liste und filtert nach Status „Entwurf". +2. BM prüft jede Bieterfrage, ändert Status auf „Intern abgestimmt". +3. Nach tatsächlicher Einreichung beim Ausschreiber: BM ändert Status auf „Eingereicht" und trägt Einreichungsdatum ein. +4. Bieterfragefrist wird auf der Seite prominent mit Restlaufzeit angezeigt. + +**Ergebnis:** Einreichungsstatus aller Bieterfragen ist nachvollziehbar; Frist wird nicht verpasst. + +--- + +### UC-BF-03 — Antwort des Ausschreibers dokumentieren und einarbeiten + +| | | +|---|---| +| **Akteur** | BM, FV | +| **Phase** | 4 | +| **Einstieg** | Ausschreibung → Bieterfragen → Bieterfrage öffnen | + +1. BM öffnet eine Bieterfrage mit Status „Eingereicht". +2. BM trägt die Antwort des Ausschreibers im Antwortfeld ein, setzt Status auf „Beantwortet". +3. BM dokumentiert die Auswirkung auf das Angebot (Freitext). +4. Falls die Antwort eine Anforderung klärt: BM verknüpft die Antwort mit der Anforderung und aktualisiert deren Erfüllungsstatus. +5. BM setzt Checkbox „Eingearbeitet" und Status auf „Eingearbeitet". + +**Ergebnis:** Jede Bieterantwort ist dauerhaft mit der Frage und der betroffenen Anforderung verknüpft; keine Information geht in E-Mail-Postfächern verloren. + +--- + +## F — Dokumente + +--- + +### UC-DO-01 — Ausschreibungsunterlage hochladen und kategorisieren + +| | | +|---|---| +| **Akteur** | BM | +| **Phase** | 1 | +| **Einstieg** | Ausschreibung → Stammdaten oder Phase 1 → „Dokument hochladen" | + +1. BM klickt „Dokument hochladen". +2. Datei-Upload-Dialog öffnet sich; BM wählt Datei(en) aus dem lokalen System (PDF, Word, Excel, ZIP). +3. Für jede Datei: Kategorie auswählen (Leistungsverzeichnis, Vertragsunterlagen, Preisblatt, Formblatt, Eignungsnachweis, Technische Anlage, Bieterinformation, Sonstiges). +4. Optional: Version, Quelle, Verantwortlicher eintragen. +5. Upload → Datei erscheint sofort in der Dokumentenliste mit Status „Hochgeladen". + +**Ergebnis:** Alle Ausschreibungsunterlagen sind zentral abgelegt, kategorisiert und von allen Beteiligten abrufbar. + +--- + +### UC-DO-02 — Neue Dokumentversion hochladen + +| | | +|---|---| +| **Akteur** | BM, FV | +| **Einstieg** | Ausschreibung → Dokumente → Dokument öffnen → „Neue Version hochladen" | + +1. Nutzer öffnet das zu aktualisierende Dokument. +2. Klickt „Neue Version hochladen" — Upload-Dialog öffnet sich. +3. Neue Datei wird hochgeladen; Versionsfeld ist vorausgefüllt (z. B. „2.0") und kann angepasst werden. +4. Speichern → Alte Version erhält Status „Ersetzt / veraltet", neue Version ist die aktive. +5. Versionsverlauf ist im Dokument-Detail als Liste sichtbar; alte Versionen bleiben abrufbar. + +**Ergebnis:** Versionierung ist nachvollziehbar ohne alte Stände zu verlieren. + +--- + +### UC-DO-03 — Dokument durch Prüfer freigeben + +| | | +|---|---| +| **Akteur** | RC, FV (Prüfer), GF | +| **Phase** | 6 | +| **Einstieg** | Ausschreibung → Dokumente → Dokument öffnen | + +1. Prüfer öffnet das zu prüfende Dokument (Status „Zu prüfen"). +2. Prüfer setzt Status auf „Geprüft". +3. Freigabeinstanz öffnet das Dokument und klickt „Freigabe erteilen" (→ UC-FR-01). +4. Dokument erhält Status „Freigegeben"; Freigabe mit Person und Zeitstempel ist dauerhaft gespeichert. + +**Ergebnis:** Dokumentenfreigabe ist lückenlos dokumentiert — wer, wann, mit welchem Kommentar. + +--- + +### UC-DO-04 — Finale Abgabeversion kennzeichnen + +| | | +|---|---| +| **Akteur** | BM | +| **Phase** | 7 | +| **Einstieg** | Ausschreibung → Abgabe → Abgabe-Checkliste | + +1. BM öffnet ein Dokument, das tatsächlich eingereicht wurde. +2. BM aktiviert Checkbox „Finale Abgabeversion". +3. System setzt den Dokumentstatus auf „Final abgegeben" und sperrt das Dokument gegen weitere Uploads in diese Version. +4. In der Abgabe-Checkliste erscheint das Dokument mit grünem Haken. + +**Ergebnis:** Eingereichte Versionen sind unveränderlich markiert; Nachweis welche Fassung abgegeben wurde ist eindeutig. + +--- + +### UC-DO-05 — Standarddokument aus der Bibliothek zuordnen + +| | | +|---|---| +| **Akteur** | BM, RC | +| **Phase** | 6 | +| **Einstieg** | Ausschreibung → Abgabe → „Standarddokument hinzufügen" | + +1. BM klickt „Standarddokument hinzufügen". +2. Suchfeld öffnet sich, durchsucht den Bibliotheksbestand nach Titel, Kategorie und Standard. +3. BM wählt das passende Dokument; Ablaufdatum und Freigabestatus werden direkt angezeigt. +4. BM bestätigt die Zuordnung → Dokument erscheint in der Dokumentenliste der Ausschreibung als Referenz (kein erneuter Upload). + +**Ergebnis:** Standarddokumente werden wiederverwendet statt jedes Mal neu hochgeladen; Bibliothek bleibt autoritative Quelle. + +--- + +## G — Preise + +--- + +### UC-PR-01 — Preispunkt erfassen + +| | | +|---|---| +| **Akteur** | PC | +| **Phase** | 5 | +| **Einstieg** | Ausschreibung → Phase 5 (Preisgestaltung) → „+ Preispunkt" | + +1. PC klickt „+ Preispunkt". +2. Formular: Leistungstyp, konkrete Leistung, Menge, Einheit, Einzelpreis, Gesamtpreis, Währung, Preisstand, einmalig/wiederkehrend. +3. PC wählt ggf. Losbezug und markiert Subunternehmeranteil. +4. PC setzt Vergleichsgewicht (Default 1,0 ist vorausgefüllt). +5. Speichern → Preispunkt erscheint in der Preisliste; gewichteter Durchschnitt für den Leistungstyp wird sofort neu berechnet. + +**Ergebnis:** Preise sind strukturiert erfasst und sofort in Auswertungen sichtbar. + +--- + +### UC-PR-02 — Vergleichsgewicht eines Preispunkts anpassen + +| | | +|---|---| +| **Akteur** | PC | +| **Einstieg** | Ausschreibung → Preise → Preispunkt öffnen | + +1. PC öffnet einen Preispunkt. +2. PC ändert das Vergleichsgewicht von 1,0 auf einen Wert zwischen 0,0 und 2,0. +3. System validiert: Werte kleiner 0,0 oder größer 2,0 werden sofort mit Fehlerhinweis abgewiesen. +4. PC trägt eine Begründung der Gewichtung ein. +5. Speichern → gewichtete Durchschnittswerte für diesen Leistungstyp werden neu berechnet. + +**Ergebnis:** Unsichere oder atypische Preispunkte verfälschen Marktpreisauswertungen nicht; besonders belastbare Werte können stärker gewichtet werden. + +--- + +### UC-PR-03 — Gewichteten Marktdurchschnitt für einen Leistungstyp aufrufen + +| | | +|---|---| +| **Akteur** | PC, BM, GF | +| **Einstieg** | Ausschreibung → Preise → Leistungstyp-Auswertung oder globaler Preisvergleich | + +1. Nutzer wählt einen Leistungstyp aus der Dropdown-Liste aller verwendeten Leistungstypen. +2. System zeigt: gewichteter Durchschnitt, ungewichteter Durchschnitt, Minimum, Maximum, Anzahl Messpunkte, Summe der Vergleichsgewichte, betrachteter Zeitraum. +3. Nutzer kann nach Ausschreibungstyp (gewonnen / verloren), Zeitraum oder Subunternehmeranteil filtern. + +**Ergebnis:** Preiseinschätzung für neue Angebote basiert auf validierten historischen Werten, nicht auf Bauchgefühl. + +--- + +### UC-PR-04 — Preisfreigabe dokumentieren + +| | | +|---|---| +| **Akteur** | GF | +| **Phase** | 5 | +| **Einstieg** | Ausschreibung → Preise → „Freigabe erteilen" | + +1. GF öffnet die Preisübersicht der Ausschreibung. +2. GF prüft Gesamtpreise, Preispunkte und Annahmen. +3. GF klickt „Preisfreigabe erteilen" → Freigabe-Dialog öffnet sich. +4. GF gibt Kommentar ein, bestätigt → Freigabe wird mit Person und Zeitstempel gespeichert (→ UC-FR-01). + +**Ergebnis:** Preisfreigabe ist revisionssicher hinterlegt; ohne Freigabe zeigt die Abgabe-Checkliste eine offene Position. + +--- + +## H — Abgabe + +--- + +### UC-AB-01 — Abgabe-Checkliste führen + +| | | +|---|---| +| **Akteur** | BM | +| **Phase** | 6, 7 | +| **Einstieg** | Ausschreibung → Phase 7 (Abgabe) | + +1. BM öffnet die Abgabe-Seite. +2. Checkliste zeigt alle benötigten Dokumente mit Status (benötigt / in Bearbeitung / zur Prüfung / geprüft / freigegeben / final). +3. BM fügt fehlende Dokumente direkt über die Checkliste hinzu (Standarddokument oder neuer Upload). +4. Erforderliche Freigaben (Teilnahme, Preis, Recht, finale Abgabe) werden mit Status angezeigt; fehlende Freigaben sind rot markiert. +5. Vollständigkeitsstatus wird oben als Fortschrittsanzeige sichtbar. + +**Ergebnis:** BM sieht auf einem Blick, was noch fehlt — kein mentaler Abgleich verschiedener Listen nötig. + +--- + +### UC-AB-02 — Abgabe mit Nachweis dokumentieren + +| | | +|---|---| +| **Akteur** | BM | +| **Phase** | 7 | +| **Einstieg** | Ausschreibung → Abgabe → „Abgabe dokumentieren" | + +1. BM klickt „Abgabe dokumentieren". +2. Formular: Abgabezeitpunkt (Datum + Uhrzeit), Abgabeplattform, Verantwortlicher. +3. BM lädt Abgabenachweis hoch (Eingangsbestätigung, Screenshot, PDF-Bestätigung, Upload-Bestätigung). +4. BM setzt Status auf „Abgegeben". +5. Speichern → Ausschreibungsstatus wechselt auf „Abgegeben"; finale Dokumente werden gesperrt. + +**Ergebnis:** Zeitpunkt, Nachweis und Verantwortlicher der Abgabe sind unwiderruflich dokumentiert. + +--- + +### UC-AB-03 — Abgabeproblem dokumentieren + +| | | +|---|---| +| **Akteur** | BM | +| **Phase** | 7 | +| **Einstieg** | Ausschreibung → Abgabe → Abgabestatus | + +1. Bei einem technischen Problem bei der Abgabe setzt BM den Abgabestatus auf „Problem bei Abgabe". +2. BM beschreibt das Problem im Kommentarfeld. +3. Status bleibt für alle sichtbar auf „Problem bei Abgabe" bis die Abgabe bestätigt wird. + +**Ergebnis:** Abgabeprobleme sind dokumentiert und nicht stillschweigend übergangen. + +--- + +## I — Nachbetrachtung + +--- + +### UC-NB-01 — Zuschlag dokumentieren und Kickoff anstoßen + +| | | +|---|---| +| **Akteur** | BM | +| **Phase** | 8 | +| **Einstieg** | Ausschreibung → Phase 8 (Nachbetrachtung) | + +1. BM öffnet die Nachbetrachtungsseite, setzt Ergebnis auf „Zuschlag gewonnen". +2. BM trägt Datum der Zuschlagsentscheidung ein. +3. System erstellt automatisch eine Aufgabe „Kickoff vorbereiten" und zeigt einen Dialog zur Benennung des Projektverantwortlichen. +4. BM benennt den PL und verknüpft relevante Angebotsunterlagen (Leistungsumfang, Annahmen, offene Punkte) mit der Übergabe-Aufgabe. + +**Ergebnis:** PL bekommt sofort eine Aufgabe mit allen relevanten Informationen; keine E-Mail-Übergabe nötig. + +--- + +### UC-NB-02 — Verlust dokumentieren mit Verlustgründen + +| | | +|---|---| +| **Akteur** | BM, VT | +| **Phase** | 8 | +| **Einstieg** | Ausschreibung → Phase 8 | + +1. BM setzt Ergebnis auf „Zuschlag verloren", trägt Datum ein. +2. BM erfasst Verlustgründe (mehrere möglich) aus einer vorgegebenen Kategorienliste plus Freitext. +3. Für jeden Verlustgrund: Score zur Verlässlichkeit (1–5) eingeben. +4. BM dokumentiert ausschlaggebende Merkmale: Preisaspekte, fehlende Referenzen, Subunternehmerentscheidungen, Marktbegleiterindizien. +5. Speichern → Daten fließen in spätere Preisauswertungen und Marktbegleiteranalysen ein. + +**Ergebnis:** Verluste werden nicht nur registriert sondern analysiert; die Erkenntnisse sind für künftige Ausschreibungen strukturiert abrufbar. + +--- + +### UC-NB-03 — Lessons Learned erfassen und Erkenntnisse markieren + +| | | +|---|---| +| **Akteur** | BM, FV, VT | +| **Phase** | 8 | +| **Einstieg** | Ausschreibung → Nachbetrachtung → „Lessons Learned" | + +1. Beteiligte tragen Lessons Learned im Freitextfeld ein (mehrere Einträge möglich). +2. Empfehlungen für künftige Ausschreibungen werden separat erfasst. +3. Jeder Eintrag kann als „Wiederverwendbar" markiert werden. +4. Als wiederverwendbar markierte Einträge erscheinen in der globalen Suche und in der Bibliothek. + +**Ergebnis:** Erfahrungswissen ist für alle zugänglich und geht nicht beim Wechsel von Beteiligten verloren. + +--- + +## J — Subunternehmer & Partner + +--- + +### UC-SU-01 — Neuen Subunternehmer anlegen + +| | | +|---|---| +| **Akteur** | BM, FV | +| **Einstieg** | Sidebar → Partner → Subunternehmer → „+ Subunternehmer" | + +1. BM klickt „+ Subunternehmer". +2. Formular: Name, Dienstleistertyp, Leistungsbereiche, Ansprechpartner, E-Mail, Mobilnummer, Präferenz (bevorzugt / zugelassen / gesperrt). +3. Optional: Adresse, Website, Zertifizierungen, Erfahrungsnotiz. +4. Speichern → Subunternehmer ist im Katalog und kann Ausschreibungen und Losen zugeordnet werden. + +**Ergebnis:** Subunternehmer-Stammdaten sind zentral gepflegt und müssen nicht bei jeder Ausschreibung neu erfasst werden. + +--- + +### UC-SU-02 — Subunternehmer einer Ausschreibung und einem Los zuordnen + +| | | +|---|---| +| **Akteur** | BM, FV | +| **Phase** | 4 | +| **Einstieg** | Ausschreibung → Los-Detail → „Subunternehmer zuordnen" | + +1. BM öffnet das Los, für das ein Subunternehmer benötigt wird. +2. Im Abschnitt „Subunternehmer" klickt BM auf „Subunternehmer zuordnen". +3. Suche öffnet sich, gefiltert auf den Subunternehmer-Katalog. +4. BM wählt den Subunternehmer und definiert die konkrete Leistung, die er beibringt. +5. BM dokumentiert: ob Zusage vorliegt, ob Nachweis eingegangen ist, ob Preis vorliegt. + +**Ergebnis:** Subunternehmer-Beteiligung ist je Los nachvollziehbar dokumentiert; offene Rückmeldungen sind sichtbar. + +--- + +### UC-SU-03 — Subunternehmer nach Leistungsbereich suchen + +| | | +|---|---| +| **Akteur** | BM, FV | +| **Einstieg** | Sidebar → Partner → Subunternehmer → Suche / Filter | + +1. Nutzer öffnet den Subunternehmer-Katalog. +2. Nutzer filtert nach Dienstleistertyp oder gibt einen Leistungsbereich im Suchfeld ein. +3. Ergebnisliste zeigt Name, Dienstleistertyp, Präferenz-Status und Erfahrungsnotiz. +4. Nutzer klickt auf einen Subunternehmer um alle bisherigen Ausschreibungen mit diesem Partner einzusehen. + +**Ergebnis:** Passende Subunternehmer werden schnell gefunden; historische Zusammenarbeit ist sofort einsehbar. + +--- + +### UC-SU-04 — Subunternehmer als gesperrt markieren + +| | | +|---|---| +| **Akteur** | BM, AD | +| **Einstieg** | Partner → Subunternehmer → Subunternehmer öffnen | + +1. BM öffnet den Subunternehmer-Datensatz. +2. BM ändert Präferenzstatus auf „Gesperrt" und trägt eine Begründung in die Erfahrungsnotiz ein. +3. Speichern → Subunternehmer erscheint in Suchergebnissen mit rotem „Gesperrt"-Badge. +4. Bei Versuch, diesen Subunternehmer einer Ausschreibung zuzuordnen, erscheint eine Warnung. + +**Ergebnis:** Negative Erfahrungen werden systemweit sichtbar gemacht; keine versehentliche Wiederbeauftragung. + +--- + +## K — Bibliothek + +--- + +### UC-BIB-01 — Compliance-Nachweis anlegen + +| | | +|---|---| +| **Akteur** | RC, AD | +| **Einstieg** | Sidebar → Bibliothek → Nachweise → „+ Nachweis" | + +1. RC klickt „+ Nachweis". +2. Formular: Titel, Kurzbeschreibung, Dokumenttyp, Kategorie, Datei-Upload, Version, Gültig ab, Gültig bis, Freigabestatus, zugehörige Standards (z. B. ISO 27001). +3. RC wählt: Für öffentliche Ausschreibungen geeignet / Für privatwirtschaftliche Ausschreibungen geeignet. +4. Speichern → Nachweis ist im Katalog und kann mit Anforderungen verknüpft werden. + +**Ergebnis:** Compliance-Nachweise sind zentral versioniert; kein Dokument wird doppelt hochgeladen. + +--- + +### UC-BIB-02 — Ablaufenden Nachweis aktualisieren + +| | | +|---|---| +| **Akteur** | RC | +| **Einstieg** | Dashboard → „Ablaufende Nachweise" oder Bibliothek → Nachweise → Filter „Bald ablaufend" | + +1. RC sieht auf dem Dashboard oder in der Bibliothek Nachweise mit `gueltig_bis` ≤ 60 Tage. +2. RC öffnet den ablaufenden Nachweis. +3. RC lädt neue Datei hoch (→ UC-DO-02 Versionierung), aktualisiert „Gültig bis". +4. Freigabe des neuen Nachweises wird erteilt (→ UC-FR-01). +5. Alte Version erhält Status „Ersetzt / veraltet". + +**Ergebnis:** Kein Nachweis läuft unbemerkt ab; Ausschreibungen, die diesen Nachweis verwenden, zeigen automatisch die aktuelle Version. + +--- + +### UC-BIB-03 — Referenz mit Whitepaper anlegen + +| | | +|---|---| +| **Akteur** | VT, BM | +| **Einstieg** | Bibliothek → Referenzen → „+ Referenz" | + +1. VT klickt „+ Referenz". +2. Formular: Referenztitel, Kunde, Branche, öffentlich/privatwirtschaftlich, Leistungsbeschreibung, Projektzeitraum, Ansprechpartner beim Referenzkunden. +3. VT lädt Whitepaper als Datei hoch und trägt Kurzfassung und Langfassung ein. +4. VT setzt Freigabestatus zur Verwendung und ggf. Einschränkungen. +5. Speichern → Referenz ist im Katalog und bei Ausschreibungen abrufbar. + +**Ergebnis:** Referenzen sind zentral gepflegt und mit Nutzungseinschränkungen versehen. + +--- + +### UC-BIB-04 — Referenz einer Ausschreibung zuordnen + +| | | +|---|---| +| **Akteur** | BM, FV | +| **Phase** | 6 | +| **Einstieg** | Ausschreibung → Abgabe → „Referenz hinzufügen" oder Anforderungsdetail → „Referenz verknüpfen" | + +1. BM klickt „Referenz hinzufügen". +2. Suchfeld öffnet sich; Suche nach Branche, Leistungsbeschreibung oder Titel. +3. BM wählt eine oder mehrere Referenzen; Freigabestatus und Nutzungseinschränkungen werden direkt angezeigt. +4. BM verknüpft die Referenz optional mit einer spezifischen Anforderung oder einem Bewertungskriterium. + +**Ergebnis:** Passende Referenzen werden gezielt ausgewählt statt händisch in E-Mails gesucht. + +--- + +### UC-BIB-05 — Entscheidungsregel anlegen + +| | | +|---|---| +| **Akteur** | AD, BM | +| **Einstieg** | Bibliothek → Entscheidungsregeln → „+ Regel" | + +1. AD klickt „+ Regel". +2. Formular: Regelname, Beschreibung, Kategorie, Gewichtung, Bewertungslogik (Freitext), Schwellenwert (optional), Empfehlung (teilnehmen / nicht teilnehmen / prüfen), Begründung, Gültigkeit. +3. AD aktiviert die Regel. +4. Speichern → Regel wird bei der nächsten Entscheidungsregelauswertung (→ UC-AS-05) angewendet. + +**Ergebnis:** Entscheidungswissen aus Erfahrungen wird systematisiert und auf neue Ausschreibungen übertragen. + +--- + +## L — Marktbegleiter + +--- + +### UC-MB-01 — Marktbegleiter anlegen + +| | | +|---|---| +| **Akteur** | VT, BM | +| **Einstieg** | Sidebar → Marktbegleiter → „+ Marktbegleiter" | + +1. VT klickt „+ Marktbegleiter". +2. Formular: Name, Kurzbeschreibung, Produkt- und Leistungsportfolio, relevante Branchen, bekannte Stärken und Schwächen, typische Formulierungen, bekannte Zertifizierungen. +3. VT trägt Quellen / Links und Vertraulichkeitsstufe ein. +4. Speichern → Marktbegleiter ist im Katalog und kann mit Ausschreibungspassagen verknüpft werden. + +**Ergebnis:** Wettbewerbswissen ist strukturiert dokumentiert und nicht auf einzelne Personen verteilt. + +--- + +### UC-MB-02 — Ausschreibungspassage einem Marktbegleiter zuordnen + +| | | +|---|---| +| **Akteur** | BM, VT | +| **Phase** | 2, 3 | +| **Einstieg** | Ausschreibung → Marktbegleiter-Analyse → „+ Passage erfassen" | + +1. BM klickt „+ Passage erfassen". +2. Formular: Textpassage aus Ausschreibungsunterlage (Copy-Paste), Fundstelle (Dokumentseite), Kategorie, zugeordneter Marktbegleiter (Auswahl aus Katalog). +3. BM begründet die Zuordnung und setzt einen Verlässlichkeitsscore (1–10). +4. BM dokumentiert Auswirkung auf Teilnahmeentscheidung, Preisstrategie und Lösungskonzept. +5. Speichern → Passage erscheint im Marktbegleiter-Profil und in der Ausschreibungsübersicht. + +**Ergebnis:** Interne Indizien auf Marktbegleiterbeeinflussung sind strukturiert gesammelt und mit Begründung nachvollziehbar. + +--- + +### UC-MB-03 — Marktbegleiter-Aufkommen auswerten + +| | | +|---|---| +| **Akteur** | VT, GF | +| **Einstieg** | Marktbegleiter → Profil → Ausschreibungspassagen | + +1. VT öffnet das Profil eines Marktbegleiters. +2. Profil zeigt alle verknüpften Ausschreibungspassagen mit Ausschreibungstitel, Datum und Verlässlichkeitsscore. +3. VT filtert nach Ausschreiber, Branche oder Zeitraum. +4. VT erkennt Muster: bei welchen Ausschreibern und mit welchen Formulierungen tritt dieser Marktbegleiter regelmäßig auf. + +**Ergebnis:** Strategische Positionierung von Wettbewerbern wird langfristig sichtbar; Teilnahmeentscheidungen sind besser informiert. + +--- + +## M — Freigaben + +--- + +### UC-FR-01 — Freigabe erteilen + +| | | +|---|---| +| **Akteur** | GF, RC, BM (je nach Freigabetyp) | +| **Einstieg** | Kontextseite (Ausschreibung / Preise / Dokument) → „Freigabe erteilen" | + +1. Freigabeinstanz öffnet die jeweilige Seite (z. B. Preise, Teilnahmeentscheidung, Dokument). +2. Klickt „Freigabe erteilen" → Freigabe-Dialog erscheint. +3. Freigabetyp ist vorausgefüllt (Teilnahme / Preis / Recht / Abgabe / Dokument / Referenz). +4. Person trägt optionalen Kommentar ein und bestätigt. +5. Freigabe wird mit Freigabetyp, Person, Datum und Uhrzeit gespeichert. + +**Ergebnis:** Freigabe ist revisionssicher dokumentiert; keine Freigabe kann nachträglich still geändert werden. + +--- + +### UC-FR-02 — Alle Freigaben einer Ausschreibung im Überblick sehen + +| | | +|---|---| +| **Akteur** | BM, GF | +| **Einstieg** | Ausschreibung → Freigaben | + +1. BM öffnet den Freigaben-Tab der Ausschreibung. +2. Liste zeigt alle Freigaben: Typ, Person, Zeitstempel, Status, Kommentar. +3. Fehlende Freigaben (z. B. Preisfreigabe noch ausstehend) werden oben als offene Punkte angezeigt. + +**Ergebnis:** BM sieht auf einen Blick welche Freigaben vorliegen und welche noch fehlen — ohne jede Seite einzeln zu prüfen. + +--- + +## N — Flexible Felder + +--- + +### UC-FF-01 — Feld eines Entitätstyps global ausblenden + +| | | +|---|---| +| **Akteur** | AD | +| **Einstieg** | Administration → Felder & Stammdaten → Entitätstyp auswählen | + +1. AD öffnet `/admin/felder/` und wählt den Entitätstyp (z. B. „Subunternehmer"). +2. Tabelle zeigt alle Felder des Typs mit Toggle-Schalter „Anzeigen / Ausblenden". +3. AD schaltet z. B. das Feld „Mobilnummer" auf „Ausblenden". +4. Speichern → Das Feld erscheint ab sofort auf keiner Subunternehmer-Detailseite mehr. +5. Bestehende Daten in ausgeblendeten Feldern bleiben erhalten; Einblenden stellt sie sofort wieder dar. + +**Ergebnis:** Die Oberfläche ist auf das wesentliche Informationsmodell des Teams zugeschnitten, ohne dass Code geändert werden muss. + +--- + +### UC-FF-02 — Feldbezeichnung umbenennen + +| | | +|---|---| +| **Akteur** | AD | +| **Einstieg** | Administration → Felder & Stammdaten → Entitätstyp → Feld anklicken | + +1. AD öffnet die Feldkonfiguration für einen Entitätstyp. +2. AD klickt auf ein Feld und trägt unter „Anzeigebezeichnung" eine eigene Beschriftung ein (z. B. „Ansprechpartner" statt „Kontaktperson"). +3. Speichern → Der neue Label erscheint sofort in allen Formularen und Detailansichten des Entitätstyps. + +**Ergebnis:** Fachsprache des Teams wird in der UI verwendet statt generischer Feldnamen. + +--- + +### UC-FF-03 — Benutzerdefiniertes Attribut an eine Instanz anhängen + +| | | +|---|---| +| **Akteur** | Alle | +| **Einstieg** | Detailseite beliebiger Entität → Abschnitt „Weitere Attribute" → „+ Attribut hinzufügen" | + +1. Nutzer scrollt auf einer Detailseite (z. B. Subunternehmer-Detail) zum Abschnitt „Weitere Attribute". +2. Klickt „+ Attribut hinzufügen" → Inline-Formular erscheint (kein Seitenreload). +3. Nutzer gibt Bezeichnung und Wert ein, wählt Datentyp (Text / Zahl / Datum / Ja–Nein / Link / E-Mail). +4. Speichern → Attribut erscheint sofort im Abschnitt „Weitere Attribute"; weitere Attribute können ergänzt werden. +5. Bestehende Attribute können bearbeitet, umsortiert und gelöscht werden. + +**Ergebnis:** Jede Instanz kann ohne Datenbankschema-Änderung um teamspezifische Informationen erweitert werden. + +--- + +## O — Feedback + +--- + +### UC-FB-01 — Feedback im Arbeitskontext einreichen + +| | | +|---|---| +| **Akteur** | Alle | +| **Einstieg** | Jede Seite → Feedback-Icon (unten rechts, persistent) | + +1. Nutzer klickt das Feedback-Icon auf einer beliebigen Seite. +2. Ein Modal öffnet sich; Seite/Kontext und ggf. aktuelle Ausschreibung sind bereits vorausgefüllt. +3. Nutzer wählt Kategorie (Fehler / Verbesserungsvorschlag / Hinweis), trägt Titel und Beschreibung ein. +4. Optional: Dringlichkeit angeben. +5. Senden → Feedbackeintrag erscheint sofort im Feedback-Backlog (→ UC-FB-02). + +**Ergebnis:** Nutzungshinweise gehen nicht verloren; der Kontext (Seite, Ausschreibung) ist automatisch mitgeliefert. + +--- + +### UC-FB-02 — Feedback-Backlog sichten und priorisieren + +| | | +|---|---| +| **Akteur** | AD, BM | +| **Einstieg** | Sidebar → Feedback-Backlog | + +1. AD öffnet den Feedback-Backlog. +2. Liste zeigt alle Einträge mit Titel, Kategorie, Dringlichkeit, Status, Erfasser, Datum und Seitenkontext. +3. AD filtert nach Kategorie „Fehler" und Status „Neu". +4. AD öffnet einen Eintrag, bewertet ihn, setzt Priorität und trägt Entscheidung + Umsetzungshinweis ein. +5. Status wird auf „In Bearbeitung", „Umgesetzt" oder „Abgelehnt" gesetzt. + +**Ergebnis:** Praxisnahes Nutzerfeedback wird systematisch verarbeitet statt in E-Mails zu verschwinden. + +--- + +## Zusammenfassung + +| Bereich | Anzahl UCs | +|---|---| +| Überblick & Dashboard | 3 | +| Ausschreibungsverwaltung | 7 | +| Lose & Anforderungen | 5 | +| Aufgaben | 4 | +| Bieterfragen | 3 | +| Dokumente | 5 | +| Preise | 4 | +| Abgabe | 3 | +| Nachbetrachtung | 3 | +| Subunternehmer & Partner | 4 | +| Bibliothek | 5 | +| Marktbegleiter | 3 | +| Freigaben | 2 | +| Flexible Felder | 3 | +| Feedback | 2 | +| **Gesamt** | **56** | diff --git a/workplans/README.md b/workplans/README.md new file mode 100644 index 0000000..3e7dd4a --- /dev/null +++ b/workplans/README.md @@ -0,0 +1,34 @@ +# Workplans — Vergabe Teilnahme + +Vollständiger Implementierungsplan in 12 Ralph-Loop-Workplans. +Jeder Workplan ist mit `/ralph-workplan workplans/WP-XXXX-slug.md` zu starten. + +## Reihenfolge + +| Workplan | Inhalt | Tasks | Abhängigkeit | +|---|---|---|---| +| WP-0001 | Projektgerüst (Django, uv, Tailwind, Docker) | 12 | — | +| WP-0002 | Alle Fachmodelle, Migrationen, Admin, Seed | 14 | WP-0001 | +| WP-0003 | Basis-UI: Shell, Templates, Template-Tags, Nav | 10 | WP-0002 | +| WP-0004 | Dashboard, Ausschreibungen CRUD | 12 | WP-0003 | +| WP-0005 | Lose und Anforderungen | 8 | WP-0004 | +| WP-0006 | Aufgaben und Bieterfragen | 6 | WP-0005 | +| WP-0007 | Dokumentenmanagement | 6 | WP-0006 | +| WP-0008 | Preise und Marktpreisauswertung | 5 | WP-0007 | +| WP-0009 | Abgabe und Nachbetrachtung | 5 | WP-0008 | +| WP-0010 | Subunternehmer, Partner, Bibliothek | 7 | WP-0009 | +| WP-0011 | Marktbegleiter-Analyse | 4 | WP-0010 | +| WP-0012 | Freigaben, Flexible Felder, Feedback, E2E-Tests | 8 | WP-0011 | + +## Start + +```bash +# Neue Session starten (Kontext muss frisch sein — /compact bei Bedarf) +/ralph-workplan workplans/WP-0001-projektgeruest.md --max-iterations 15 +``` + +## Referenzdokumente + +- `wiki/ProductRequirementsDocument.md` — fachliche Anforderungen +- `wiki/ArchitectureBlueprint.md` — technische Architektur, Modelle, URL-Struktur +- `wiki/UseCaseCatalog.md` — 56 Use Cases mit konkreten UI-Abläufen diff --git a/workplans/WP-0001-projektgeruest.md b/workplans/WP-0001-projektgeruest.md new file mode 100644 index 0000000..a996535 --- /dev/null +++ b/workplans/WP-0001-projektgeruest.md @@ -0,0 +1,405 @@ +--- +id: WP-0001 +title: Projektgerüst — Django-Setup, Tailwind, Dev-Stack +status: todo +phase: 1-of-12 +created: "2026-05-08" +--- + +# WP-0001 — Projektgerüst + +Legt das vollständige Django-Projektgerüst an: uv, Projektstruktur, Settings, +alle App-Hüllen, Tailwind CSS v4 via Vite, HTMX + Alpine.js, Docker Compose +für PostgreSQL, pytest-django, Makefile. + +**Referenzdokumente:** `wiki/ArchitectureBlueprint.md` Abschnitte 2 und 3. +**Arbeitsverzeichnis:** `/home/worsch/vergabe-teilnahme/` + +--- + +```task +id: WP-0001-T01 +title: pyproject.toml und uv-Projektstruktur anlegen +status: todo + +Erstelle `pyproject.toml` mit uv als Package-Manager. + +Abhängigkeiten (production): + django>=5.2, psycopg[binary]>=3.2, django-storages>=1.14, + whitenoise>=6.7, python-decouple>=3.8 + +Abhängigkeiten (dev): + pytest-django>=4.8, pytest-cov>=5.0, factory-boy>=3.3, + ruff>=0.4, mypy>=1.10, django-stubs>=5.0 + +Python: >=3.12 + +Erstelle außerdem `.python-version` mit `3.12`. +Führe `uv sync` aus und bestätige, dass die virtuelle Umgebung erstellt wird. +``` + +```task +id: WP-0001-T02 +title: Django-Projekt initialisieren und Settings-Struktur anlegen +status: todo + +Führe `uv run django-admin startproject vergabe_teilnahme .` aus +(Punkt am Ende — kein verschachteltes Projektverzeichnis). + +Erstelle `vergabe_teilnahme/settings/` mit: +- `__init__.py` (leer) +- `base.py` — gemeinsame Settings (INSTALLED_APPS, TEMPLATES, STATIC, MEDIA, + AUTH_USER_MODEL = 'accounts.Mitarbeiter', DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField', + LANGUAGE_CODE = 'de-de', TIME_ZONE = 'Europe/Berlin', USE_I18N = True, USE_TZ = True) +- `dev.py` — importiert base, setzt DEBUG=True, ALLOWED_HOSTS=['*'], + DATABASE aus python-decouple .env +- `prod.py` — importiert base, DEBUG=False, ALLOWED_HOSTS aus Env, + WhiteNoise-Middleware, SECURE_* Flags + +Passe `manage.py` und `vergabe_teilnahme/wsgi.py` auf +`DJANGO_SETTINGS_MODULE = 'vergabe_teilnahme.settings.dev'` an. +``` + +```task +id: WP-0001-T03 +title: .env.example und PostgreSQL-Konfiguration +status: todo + +Erstelle `.env.example`: +``` +DATABASE_URL=postgres://vergabe:vergabe@localhost:5432/vergabe_db +SECRET_KEY=change-me-in-production +DEBUG=True +ALLOWED_HOSTS=localhost,127.0.0.1 +MEDIA_ROOT=media/ +MAX_UPLOAD_SIZE=52428800 +``` + +In `settings/base.py` lese DATABASE_URL via `python-decouple` und parse mit +`dj-database-url` (füge `dj-database-url>=2.1` zu pyproject.toml hinzu). + +Erstelle `.env` (nur lokal, in .gitignore) mit Entwicklungswerten. +Prüfe dass `.env` und `*.sqlite3` in `.gitignore` enthalten sind. +``` + +```task +id: WP-0001-T04 +title: Alle Django-Apps anlegen +status: todo + +Erstelle folgende Apps mit `uv run manage.py startapp `: + core, accounts, ausschreibungen, lose, aufgaben, + dokumente, preise, partner, bibliothek, marktbegleiter, + nachbetrachtung, feedback + +Verschiebe jede App in ein eigenes Unterverzeichnis: + `vergabe_teilnahme/apps//` + +Passe in jeder App `apps.py` den `name` auf `vergabe_teilnahme.apps.` an. + +Füge alle Apps zu `INSTALLED_APPS` in `settings/base.py` hinzu. +Stelle sicher, dass `django.contrib.contenttypes` ebenfalls in INSTALLED_APPS ist +(wird für GenericForeignKey im core-Modell benötigt). +``` + +```task +id: WP-0001-T05 +title: Tailwind CSS v4 via Vite integrieren +status: todo + +Erstelle `package.json` im Projektwurzelverzeichnis: +```json +{ + "scripts": { + "dev": "vite build --watch", + "build": "vite build" + }, + "devDependencies": { + "vite": "^5.0", + "@tailwindcss/vite": "^4.0", + "tailwindcss": "^4.0" + } +} +``` + +Erstelle `vite.config.js`: +```js +import { defineConfig } from 'vite' +import tailwindcss from '@tailwindcss/vite' +export default defineConfig({ + plugins: [tailwindcss()], + build: { outDir: 'static/dist', emptyOutDir: true, + rollupOptions: { input: 'static/src/main.css' } } +}) +``` + +Erstelle `static/src/main.css`: +```css +@import "tailwindcss"; +@layer base { /* German-app base resets */ } +@layer components { + .card { @apply bg-white rounded-xl border border-slate-200 shadow-sm p-6; } + .btn-primary { @apply bg-brand-500 text-white px-4 py-2 rounded-lg hover:bg-brand-600 transition-colors; } + .btn-secondary { @apply bg-white text-slate-700 border border-slate-300 px-4 py-2 rounded-lg hover:bg-slate-50; } + .btn-danger { @apply bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700; } + .btn-ghost { @apply text-slate-600 px-3 py-2 rounded-lg hover:bg-slate-100; } + .field-row { @apply grid grid-cols-3 gap-4 py-3 border-b border-slate-100 last:border-0; } + .field-label { @apply text-sm font-medium text-slate-500 col-span-1; } + .field-value { @apply text-sm text-slate-900 col-span-2; } + .phase-badge { @apply inline-flex items-center justify-center w-7 h-7 rounded-full text-sm font-bold; } + .phase-todo { @apply phase-badge bg-slate-200 text-slate-500; } + .phase-active { @apply phase-badge bg-brand-500 text-white; } + .phase-done { @apply phase-badge bg-green-500 text-white; } + .phase-warn { @apply phase-badge bg-amber-400 text-amber-900; } + .section-title { @apply text-base font-semibold text-slate-900 mb-4; } + .page-title { @apply text-2xl font-bold text-slate-900; } + .form-input { @apply w-full rounded-lg border border-slate-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent; } + .form-label { @apply block text-sm font-medium text-slate-700 mb-1; } + .table-base { @apply w-full text-sm text-left; } + .table-header { @apply bg-slate-50 text-slate-500 font-medium text-xs uppercase tracking-wide; } + .table-row { @apply border-t border-slate-100 hover:bg-slate-50 transition-colors; } +} +``` + +Füge CSS-Theme-Token für `brand` in `main.css` hinzu: +```css +@theme { + --color-brand-50: #f0f4ff; + --color-brand-100: #dce7ff; + --color-brand-500: #3b5bdb; + --color-brand-600: #2f4ac7; + --color-brand-700: #2541b2; + --color-brand-900: #152d99; +} +``` + +Konfiguriere Django STATICFILES_DIRS und STATIC_ROOT in settings/base.py. +Füge `STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'` +in settings/prod.py ein. +Führe `npm install` aus. +``` + +```task +id: WP-0001-T06 +title: HTMX und Alpine.js einbinden +status: todo + +Lade HTMX und Alpine.js als lokale Vendor-Dateien (keine CDN-Abhängigkeit): +- `static/vendor/htmx/htmx.min.js` — HTMX 2.x (von unpkg herunterladen) +- `static/vendor/alpinejs/alpine.min.js` — Alpine.js 3.x + +Füge in `settings/base.py` hinzu: +```python +STATICFILES_DIRS = [BASE_DIR / 'static'] +``` + +Die Einbindung im base.html-Template erfolgt in WP-0003. +Erstelle hier nur die Verzeichnisstruktur und die Dateien. +Prüfe: `ls static/vendor/htmx/` und `ls static/vendor/alpinejs/` sollten die Dateien zeigen. +``` + +```task +id: WP-0001-T07 +title: Docker Compose für Entwicklungs-PostgreSQL +status: todo + +Erstelle `docker-compose.dev.yml`: +```yaml +services: + db: + image: postgres:16-alpine + environment: + POSTGRES_DB: vergabe_db + POSTGRES_USER: vergabe + POSTGRES_PASSWORD: vergabe + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data +volumes: + postgres_data: +``` + +Erstelle außerdem `docker-compose.test.yml` für CI (gleiche Konfiguration, +anderer DB-Name: `vergabe_test`). + +Starte die DB: `docker compose -f docker-compose.dev.yml up -d` +Prüfe Verbindung: `uv run manage.py check --database default` +``` + +```task +id: WP-0001-T08 +title: pytest-django konfigurieren +status: todo + +Füge in `pyproject.toml` hinzu: +```toml +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "vergabe_teilnahme.settings.dev" +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "--tb=short -q" + +[tool.ruff] +line-length = 100 +target-version = "py312" +select = ["E", "F", "I", "N", "UP"] +``` + +Erstelle `conftest.py` im Projektwurzel: +```python +import pytest +@pytest.fixture +def mitarbeiter(db): + from vergabe_teilnahme.apps.accounts.models import Mitarbeiter + return Mitarbeiter.objects.create_user( + username='testuser', password='testpass', first_name='Test', last_name='User' + ) +``` + +Prüfe: `uv run pytest --co -q` (keine Tests vorhanden, aber Konfiguration valide). +``` + +```task +id: WP-0001-T09 +title: Makefile für häufige Dev-Commands +status: todo + +Erstelle `Makefile` im Projektwurzel: +```makefile +.PHONY: dev db migrate shell test lint css + +db: + docker compose -f docker-compose.dev.yml up -d + +dev: db + uv run manage.py runserver 0.0.0.0:8000 + +css: + npm run dev + +migrate: + uv run manage.py makemigrations + uv run manage.py migrate + +shell: + uv run manage.py shell_plus 2>/dev/null || uv run manage.py shell + +test: + uv run pytest + +lint: + uv run ruff check . + uv run mypy vergabe_teilnahme/ + +createsuperuser: + uv run manage.py createsuperuser + +collectstatic: + uv run manage.py collectstatic --noinput +``` + +Prüfe: `make db` startet PostgreSQL, `make migrate` läuft fehlerfrei durch +(zu diesem Zeitpunkt noch ohne Fachmodelle — nur Django-Default-Migrationen). +``` + +```task +id: WP-0001-T10 +title: Django URL-Grundkonfiguration und Health-Check +status: todo + +Editiere `vergabe_teilnahme/urls.py`: +```python +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static +from django.http import JsonResponse + +def health(request): + return JsonResponse({'status': 'ok'}) + +urlpatterns = [ + path('admin/', admin.site.urls), + path('health/', health), + # Module-URLs werden in späteren Workplans ergänzt +] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +``` + +Füge in `settings/base.py` hinzu: +```python +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' +``` + +Prüfe: `make dev` startet ohne Fehler, `curl http://localhost:8000/health/` +gibt `{"status": "ok"}` zurück. +``` + +```task +id: WP-0001-T11 +title: CLAUDE.md mit Build-Commands aktualisieren +status: todo + +Aktualisiere `/home/worsch/vergabe-teilnahme/CLAUDE.md` um einen Abschnitt +"## Entwicklungs-Commands": + +```markdown +## Entwicklungs-Commands + +```bash +make db # PostgreSQL via Docker starten +make dev # Django-Dev-Server (Port 8000) +make css # Tailwind CSS im Watch-Modus +make migrate # Migrations generieren und ausführen +make test # pytest ausführen +make lint # ruff + mypy +uv run manage.py test # Einzelne App testen +uv run pytest tests/.py # Einzelne Testdatei +``` + +## Projektstruktur + +``` +vergabe_teilnahme/ +├── apps/ # Alle Django-Apps +│ ├── core/ # FlexibleModel, CustomAttribute, EntityFieldConfig, Freigabe +│ ├── accounts/ # Mitarbeiter (AbstractUser) +│ └── ... # je eine App pro Fachdomäne +├── settings/ # base.py, dev.py, prod.py +└── urls.py + +static/ +├── src/main.css # Tailwind-Quelldatei +├── vendor/ # HTMX, Alpine.js +└── dist/ # Build-Output (gitignored) + +workplans/ # Ralph-Loop-Workplans +wiki/ # PRD, Blueprint, Use-Case-Katalog +``` +``` + +Füge außerdem `static/dist/` zu `.gitignore` hinzu. +``` + +```task +id: WP-0001-T12 +title: Erstes `uv run manage.py migrate` und Smoke-Test +status: todo + +Führe die gesamte initiale Setup-Sequenz durch und verifiziere: + +1. `make db` → PostgreSQL läuft +2. `uv run manage.py migrate` → alle Django-Default-Migrationen laufen sauber durch +3. `uv run manage.py check` → keine Fehler +4. `uv run pytest` → 0 Tests gesammelt, kein Fehler +5. `npm run build` → `static/dist/` enthält die kompilierte CSS-Datei +6. `make dev` → Server startet, `/health/` antwortet mit 200 + +Notiere etwaige Fehler und behebe sie. Erst wenn alle 6 Checks bestanden sind +gilt dieser Task als erledigt. +``` diff --git a/workplans/WP-0002-fachmodelle.md b/workplans/WP-0002-fachmodelle.md new file mode 100644 index 0000000..9a34623 --- /dev/null +++ b/workplans/WP-0002-fachmodelle.md @@ -0,0 +1,503 @@ +--- +id: WP-0002 +title: Fachmodelle — alle Django-Models, Migrationen, Admin +status: todo +phase: 2-of-12 +created: "2026-05-08" +depends_on: WP-0001 +--- + +# WP-0002 — Fachmodelle + +Implementiert alle Django-Modelle gemäß `wiki/ArchitectureBlueprint.md` Abschnitt 5. +Alle Modelle erben von `FlexibleModel`. Am Ende: alle Migrationen, Admin-Registrierung +und ein Management-Command für Seed-Daten. + +**Arbeitsverzeichnis:** `/home/worsch/vergabe-teilnahme/` +**Apps-Pfad:** `vergabe_teilnahme/apps/` + +--- + +```task +id: WP-0002-T01 +title: Accounts-App: Mitarbeiter-Modell (AbstractUser) +status: todo + +Datei: `vergabe_teilnahme/apps/accounts/models.py` + +```python +from django.contrib.auth.models import AbstractUser +from django.db import models + +class Mitarbeiter(AbstractUser): + ROLLE_CHOICES = [ + ('bid_manager', 'Bid Manager'), + ('fachexperte', 'Fachverantwortlicher'), + ('vertrieb', 'Vertrieb / Account Management'), + ('pricing', 'Pricing / Controlling'), + ('recht', 'Recht / Compliance'), + ('geschaeftsfuehrung', 'Geschäftsführung'), + ('projektleitung','Projektleitung Umsetzung'), + ('admin', 'Administrator'), + ] + rolle = CharField(max_length=30, choices=ROLLE_CHOICES, blank=True) + mobilnummer = CharField(max_length=50, blank=True) + organisationseinheit = CharField(max_length=200, blank=True) + + def __str__(self): + return self.get_full_name() or self.username +``` + +Registriere in `accounts/admin.py` mit `UserAdmin`. +Setze `AUTH_USER_MODEL = 'accounts.Mitarbeiter'` in `settings/base.py` (bereits in T02 von WP-0001 vorbereitet — prüfen). +``` + +```task +id: WP-0002-T02 +title: Core-App: FlexibleModel-Mixin und Basis-Infrastruktur +status: todo + +Datei: `vergabe_teilnahme/apps/core/models.py` + +Implementiere: +1. `FlexibleModel(Model)` — abstract base: + - Hat `GenericRelation` auf `CustomAttribute` + - Methode `get_hidden_fields()` — liest `EntityFieldConfig` für `self._meta.model_name` + - Methode `get_visible_field_names()` — gibt Liste nicht-ausgeblendeter Felder zurück + - `class Meta: abstract = True` + +2. `EntityFieldConfig(Model)` — globale Feldkonfiguration pro Entitätstyp: + - `entity_type` CharField(100) — model_name slug (z. B. 'subunternehmer') + - `field_name` CharField(100) + - `is_hidden` BooleanField(default=False) + - `display_label` CharField(200, blank=True) — Umbenennung + - `sort_order` PositiveSmallIntegerField(default=0) + - `Meta: unique_together = ('entity_type', 'field_name')` + +3. `CustomAttribute(Model)` — generisches Key-Value-Attribut: + - `content_type` FK(ContentType, CASCADE) + - `object_id` PositiveIntegerField() + - `content_object` GenericForeignKey() + - `key` CharField(100) — slug + - `label` CharField(200) — Anzeigename + - `value` TextField(blank=True) + - `data_type` CharField(20, choices=[text/number/date/boolean/url/email], default='text') + - `sort_order` PositiveSmallIntegerField(default=0) + - `created_at` DateTimeField(auto_now_add=True) + - `Meta: ordering = ['sort_order', 'created_at']` + - `Index` auf `(content_type, object_id)` + +4. `Freigabe(Model)` — generische Freigabe: + - `content_type` FK(ContentType, CASCADE) + - `object_id` PositiveIntegerField() + - `content_object` GenericForeignKey() + - `freigabe_typ` CharField(30, choices=[teilnahme/ablehnung/recht/preis/abgabe/standarddokument/referenz]) + - `freigebende_person` FK(settings.AUTH_USER_MODEL, PROTECT) + - `status` CharField(20, choices=[erteilt/abgelehnt/ausstehend], default='erteilt') + - `kommentar` TextField(blank=True) + - `timestamp` DateTimeField(auto_now_add=True) + - `Meta: ordering = ['-timestamp']` + +Alle vier in `core/admin.py` registrieren. +``` + +```task +id: WP-0002-T03 +title: Core-App: services.py mit Utility-Funktionen +status: todo + +Datei: `vergabe_teilnahme/apps/core/services.py` + +Implementiere folgende Funktionen (ohne Django-Import-Fehler zur Importzeit): + +```python +from datetime import date +from decimal import Decimal + +def get_deadline_warnings(ausschreibung): + """Gibt Liste von Warnungen für nahende Fristen zurück.""" + warnings = [] + heute = date.today() + if ausschreibung.bieterfragen_bis: + delta = (ausschreibung.bieterfragen_bis - heute).days + if delta <= 3: + warnings.append({'typ': 'bieterfragen', 'tage': delta, 'farbe': 'red' if delta <= 1 else 'amber'}) + if ausschreibung.abgabe_bis: + delta = (ausschreibung.abgabe_bis.date() - heute).days + if delta <= 14: + warnings.append({'typ': 'abgabe', 'tage': delta, 'farbe': 'red' if delta <= 3 else 'amber'}) + return warnings + + +def gewichteter_durchschnitt(preispunkte, feld='einzelpreis'): + """Berechnet gewichteten Durchschnitt für Preispunkte. Gibt None zurück wenn keine verwertbaren Punkte.""" + relevante = [p for p in preispunkte + if getattr(p, feld) is not None and p.vergleichsgewicht > 0] + if not relevante: + return None + summe_gewichte = sum(p.vergleichsgewicht for p in relevante) + if summe_gewichte == 0: + return None + summe = sum(getattr(p, feld) * p.vergleichsgewicht for p in relevante) + werte = [getattr(p, feld) for p in relevante] + return { + 'wert': summe / summe_gewichte, + 'summe_gewichte': summe_gewichte, + 'anzahl': len(relevante), + 'minimum': min(werte), + 'maximum': max(werte), + 'ungewichtet': sum(werte) / len(werte), + } +``` + +Schreibe Tests in `vergabe_teilnahme/apps/core/tests/test_services.py`: +- Test gewichteter_durchschnitt mit Beispiel aus Blueprint (100×1,0 + 80×0,2 + 110×1,2 = 103,33) +- Test mit leerem Input → None +- Test mit allen Gewichten 0 → None +``` + +```task +id: WP-0002-T04 +title: Ausschreibungen-App: Ausschreibung-Modell +status: todo + +Datei: `vergabe_teilnahme/apps/ausschreibungen/models.py` + +Implementiere `Ausschreibung(FlexibleModel)` gemäß Blueprint Abschnitt 5.1. +Import von FlexibleModel: `from vergabe_teilnahme.apps.core.models import FlexibleModel` + +Felder exakt wie im Blueprint definiert (STATUS_CHOICES 1-13, TEILNAHME_CHOICES, +alle Datums-/Fristen-Felder, erstellt_am/geaendert_am auto). + +Methoden: +- `__str__` → `self.titel` +- `property ist_aktiv` → status in range(1, 10) +- `property naechste_frist` → das nächste nicht-vergangene Datum aus (bieterfragen_bis, abgabe_bis.date()) +- `property phase_nummer` → min(8, max(1, self.status)) — Phasennummer für Navigator + +`Meta: ordering = ['-erstellt_am']` + +Admin-Registrierung mit list_display=['titel', 'ausschreiber', 'status', 'abgabe_bis']. +``` + +```task +id: WP-0002-T05 +title: Lose-App: Los- und Anforderung-Modell +status: todo + +Datei: `vergabe_teilnahme/apps/lose/models.py` + +`Los(FlexibleModel)`: +- `ausschreibung` FK(Ausschreibung, CASCADE, related_name='lose') +- `losnummer` CharField(50) +- `lostitel` CharField(300) +- `beschreibung` TextField(blank=True) +- `abgrenzung` TextField(blank=True) +- `zustaendiger` FK('accounts.Mitarbeiter', null=True, blank=True, SET_NULL) +- `teilnahme` BooleanField(null=True) # None = offen +- `status` CharField(50, blank=True) +- `Meta: ordering = ['losnummer']` +- `__str__` → `f"Los {self.losnummer}: {self.lostitel}"` + +`Anforderung(FlexibleModel)`: +- `ausschreibung` FK(Ausschreibung, CASCADE) +- `los` FK(Los, null=True, blank=True, SET_NULL, related_name='anforderungen') +- Alle Felder aus Blueprint 5.3 (titel, beschreibung, quelle_im_dokument, kategorie, + verbindlichkeit MUSS_CHOICES, ausschlusskriterium, bewertungskriterium, + zustaendiger FK, erfuellungsstatus CHOICES, nachweis_erforderlich) +- `dokumente` M2M('dokumente.Dokument', blank=True) +- `nachweise` M2M('bibliothek.Nachweis', blank=True) +- `__str__` → `self.titel` + +Admin für beide Modelle. +``` + +```task +id: WP-0002-T06 +title: Aufgaben-App: Aufgabe- und Bieterfrage-Modell +status: todo + +Datei: `vergabe_teilnahme/apps/aufgaben/models.py` + +`Aufgabe(FlexibleModel)` mit allen Feldern aus Blueprint 5.4: +TYP_CHOICES (fachlich/rechtlich/kaufmaennisch/technisch/subunternehmer/dokument/preis) +STATUS_CHOICES (offen/in_bearbeitung/wartend_intern/wartend_sub/wartend_ausschreiber/erledigt/verworfen/ueberfaellig) +FKs auf Ausschreibung (CASCADE), Los (null/blank/SET_NULL), Anforderung, Bieterfrage, Dokument. +prioritaet PositiveSmallIntegerField(default=2, choices=[(1,'Hoch'),(2,'Mittel'),(3,'Niedrig')]) +frist DateField(null=True, blank=True) +verantwortlicher FK(Mitarbeiter, null=True, SET_NULL) +ergebnis TextField(blank=True) +Meta: ordering = ['prioritaet', 'frist'] + +property `ist_ueberfaellig`: frist < date.today() and status not in ['erledigt', 'verworfen'] + +`Bieterfrage(FlexibleModel)` mit allen Feldern aus Blueprint 5.5: +STATUS_CHOICES (entwurf/abgestimmt/eingereicht/beantwortet/eingearbeitet) +FKs auf Ausschreibung (CASCADE), Anforderung (null/blank), Dokument (null/blank) +prioritaet, einreichungsdatum, antwort, auswirkung_angebot, eingearbeitet + +Admin für beide Modelle. +``` + +```task +id: WP-0002-T07 +title: Dokumente-App: Dokument-Modell mit Datei-Upload +status: todo + +Datei: `vergabe_teilnahme/apps/dokumente/models.py` + +`Dokument(FlexibleModel)` mit allen Feldern aus Blueprint 5.6: +- `datei` FileField(upload_to='dokumente/%Y/%m/') +- `dateiname` CharField(300) — wird beim Upload automatisch aus datei.name befüllt +- Alle Status-/Kategorie-Choices +- `ausschreibung` FK(null=True, blank=True) +- `los` FK(Los, null=True, blank=True) +- `verantwortlicher`, `pruefer` FK(Mitarbeiter) +- `finale_abgabeversion` BooleanField(default=False) +- `upload_datum` DateTimeField(auto_now_add=True) +- `GenericRelation(Freigabe)` für direkte Freigaben-Abfrage + +Signal `pre_save`: falls `finale_abgabeversion` von False auf True gesetzt wird, +setze `status='final_abgegeben'`. + +Datei-Validierung als Modell-Methode `clean()`: +Erlaubte MIME-Typen: pdf, docx, xlsx, zip, png, jpg (prüfe gegen Dateiendung, keine MIME-Detection in v1) +Maximalgröße: settings.MAX_UPLOAD_SIZE (Default: 50 MB) + +Admin-Registrierung. +``` + +```task +id: WP-0002-T08 +title: Preise-App: Preispunkt-Modell +status: todo + +Datei: `vergabe_teilnahme/apps/preise/models.py` + +`Preispunkt(FlexibleModel)` gemäß Blueprint 5.7: +- `ausschreibung` FK(Ausschreibung, CASCADE) +- `los` FK(Los, null=True, blank=True, SET_NULL) +- `leistungstyp` CharField(200) — freier Text, z. B. "Schulung", "Lizenz", "Betrieb" +- `konkrete_leistung` CharField(400) +- `mengeneinheit`, `waehrung` CharFields mit Defaults +- `menge`, `einzelpreis`, `gesamtpreis` DecimalFields(14, 2 bzw. 4, null=True, blank=True) +- `preisstand` DateField(null=True, blank=True) +- `wiederkehrend` BooleanField(default=False) +- `laufzeitbezug` CharField(100, blank=True) +- `subunternehmeranteil` BooleanField(default=False) +- `subunternehmer` FK('partner.Subunternehmer', null=True, blank=True, SET_NULL) +- `vergleichsgewicht` DecimalField(3, 1, default=Decimal('1.0')) +- `gewichtungsbegruendung` TextField(blank=True) +- `kommentar` TextField(blank=True) +- `ausschreibung_gewonnen` BooleanField(null=True) + +Modell-Validierung `clean()`: +- `vergleichsgewicht` muss zwischen Decimal('0.0') und Decimal('2.0') liegen, + sonst ValidationError("Vergleichsgewicht muss zwischen 0,0 und 2,0 liegen.") + +`Meta: ordering = ['leistungstyp', 'konkrete_leistung']` + +Admin mit list_display=['konkrete_leistung', 'leistungstyp', 'einzelpreis', 'vergleichsgewicht']. +``` + +```task +id: WP-0002-T09 +title: Partner-App: Subunternehmer und Dienstleistertyp +status: todo + +Datei: `vergabe_teilnahme/apps/partner/models.py` + +`Dienstleistertyp(FlexibleModel)`: +- name CharField(200), beschreibung TextField(blank=True) +- typische_leistungen TextField(blank=True), typische_nachweise TextField(blank=True) +- relevante_standards TextField(blank=True), typische_preisbestandteile TextField(blank=True) +- bemerkungen TextField(blank=True) +- `__str__` → name + +`Subunternehmer(FlexibleModel)`: +- Alle Felder aus Blueprint 5.9 +- PRAEFERENZ_CHOICES: bevorzugt/zugelassen/gesperrt (default='zugelassen') +- `dienstleistertyp` FK(Dienstleistertyp, null=True, blank=True, SET_NULL) +- `bisherige_ausschreibungen` M2M(Ausschreibung, blank=True, through='SubunternehmerZuordnung') + +`SubunternehmerZuordnung(Model)` — Durchgangstabelle (kein FlexibleModel): +- `subunternehmer` FK(Subunternehmer, CASCADE) +- `ausschreibung` FK(Ausschreibung, CASCADE) +- `los` FK(Los, null=True, blank=True, SET_NULL) +- `konkrete_leistung` CharField(300, blank=True) +- `zusage_vorhanden` BooleanField(default=False) +- `nachweis_eingegangen` BooleanField(default=False) +- `preis_vorhanden` BooleanField(default=False) +- `kommentar` TextField(blank=True) + +Admin für alle drei Modelle. +``` + +```task +id: WP-0002-T10 +title: Bibliothek-App: Nachweis, Referenz, Leistungsblatt, Entscheidungsregel +status: todo + +Datei: `vergabe_teilnahme/apps/bibliothek/models.py` + +`Nachweis(FlexibleModel)` — Compliance-Nachweise: +Alle Felder aus Blueprint 6.10 (titel, kurzbeschreibung, dokumenttyp, kategorie, +datei FileField, version, gueltig_ab/bis DateFields, eigentuemer FK(Mitarbeiter), +freigabestatus CharField, letzte_pruefung DateField, zugehoerige_standards TextField, +vertraulichkeit CharField, sprache CharField default='de', +fuer_oeffentliche BooleanField, fuer_privatwirtschaftliche BooleanField) +property `ist_abgelaufen`: gueltig_bis < date.today() if gueltig_bis else False + +`Referenz(FlexibleModel)` — Projekt-Referenzen: +Alle Felder aus Blueprint 6.11 (referenztitel, kunde, branche, oeffentlich_oder_privat, +leistungsbeschreibung, eingesetzte_produkte, projektzeitraum CharField, +vertragsvolumen DecimalField(null=True), ansprechpartner_referenzkunde, +freigabestatus_verwendung, vertraulichkeit, whitepaper FileField(null=True), +kurzfassung TextField, langfassung TextField, +verwendbar_fuer_ausschreibungen BooleanField, einschraenkungen_verwendung TextField) +leistungsblaetter M2M('Leistungsblatt', blank=True) + +`Leistungsblatt(FlexibleModel)`: +produktfunktion, beschreibung, leistungsumfang, grenzen_ausschluesse, +technische_voraussetzungen, typische_nachweise TextField(blank=True), +version CharField, eigentuemer FK(Mitarbeiter, null=True) + +`Entscheidungsregel(FlexibleModel)`: +regelname, beschreibung, kategorie, gewichtung DecimalField(5,2, default=1.0), +bewertungslogik TextField, schwellenwert DecimalField(null=True), +empfehlung CharField(choices=[teilnehmen/nicht_teilnehmen/pruefen]), +begruendung TextField, gueltig_von/bis DateFields(null=True), aktiv BooleanField(default=True), +verantwortlicher FK(Mitarbeiter, null=True) + +Admin für alle vier Modelle. +``` + +```task +id: WP-0002-T11 +title: Marktbegleiter-App: Marktbegleiter und Ausschreibungspassage +status: todo + +Datei: `vergabe_teilnahme/apps/marktbegleiter/models.py` + +`Marktbegleiter(FlexibleModel)` — alle Felder aus Blueprint 6.15: +name CharField(300), kurzbeschreibung TextField, +produkt_leistungsportfolio TextField(blank=True), +relevante_branchen TextField(blank=True), +bekannte_staerken TextField(blank=True), bekannte_schwaechen TextField(blank=True), +typische_formulierungen TextField(blank=True), +typische_leistungsmerkmale TextField(blank=True), +bekannte_zertifizierungen TextField(blank=True), +bekannte_referenzen TextField(blank=True), +quellen_links TextField(blank=True), letzte_aktualisierung DateField(null=True), +aktualisierungsstatus CharField(50, blank=True), +interne_notizen TextField(blank=True), +vertraulichkeit CharField(20, choices=[intern/streng_vertraulich], default='intern') + +`Ausschreibungspassage(FlexibleModel)` — alle Felder aus Blueprint 6.16: +ausschreibung FK(Ausschreibung, CASCADE, related_name='passagen') +dokument FK(Dokument, null=True, blank=True, SET_NULL) +fundstelle CharField(200, blank=True) +passage TextField() +kategorie CharField(100, blank=True) +marktbegleiter FK(Marktbegleiter, CASCADE, related_name='passagen') +begruendung_zuordnung TextField() +verlaesslichkeitsscore PositiveSmallIntegerField( + validators=[MinValueValidator(1), MaxValueValidator(10)]) +auswirkung_entscheidung TextField(blank=True) +auswirkung_preisstrategie TextField(blank=True) +auswirkung_loesungskonzept TextField(blank=True) +erfasst_von FK(Mitarbeiter, null=True, SET_NULL) +erfassungsdatum DateField(auto_now_add=True) + +Admin für beide Modelle. +``` + +```task +id: WP-0002-T12 +title: Nachbetrachtung- und Feedback-Modelle +status: todo + +Datei: `vergabe_teilnahme/apps/nachbetrachtung/models.py` + +`Nachbetrachtung(FlexibleModel)`: +- `ausschreibung` OneToOneField(Ausschreibung, CASCADE, related_name='nachbetrachtung') +- `ergebnis` CharField(20, choices=[gewonnen/verloren/aufgehoben/offen/zurueckgezogen]) +- `zuschlagsdatum` DateField(null=True, blank=True) +- `abgegebene_unterlagen` TextField(blank=True) +- `abgegebene_preise` TextField(blank=True) +- `verlustgruende` JSONField(default=list) + # Format: [{"grund": "...", "kategorie": "preis|referenz|...", "verlaesslichkeit": 1-5}] +- `ausschlaggebende_zuschlagsmerkmale` TextField(blank=True) +- `lessons_learned` TextField(blank=True) +- `empfehlungen` TextField(blank=True) +- `wiederverwendbare_erkenntnisse_markiert` BooleanField(default=False) +- `projektverantwortlicher` FK(Mitarbeiter, null=True, blank=True, SET_NULL) + +Datei: `vergabe_teilnahme/apps/feedback/models.py` + +`Feedbackeintrag(FlexibleModel)` — alle Felder aus Blueprint 6.18: +titel, beschreibung TextField, seite_kontext CharField(500), +ausschreibung FK(Ausschreibung, null=True, blank=True, SET_NULL), +kategorie CharField(choices=[fehler/verbesserung/hinweis]), +dringlichkeit CharField(choices=[niedrig/mittel/hoch/kritisch], default='mittel'), +prioritaet PositiveSmallIntegerField(default=2), +status CharField(choices=[neu/in_bearbeitung/umgesetzt/abgelehnt], default='neu'), +erfasst_von FK(Mitarbeiter, null=True, SET_NULL), +datum DateTimeField(auto_now_add=True), +bewertung TextField(blank=True), entscheidung TextField(blank=True), +umsetzungshinweis TextField(blank=True) + +Admin für beide Modelle. +``` + +```task +id: WP-0002-T13 +title: Alle Migrationen generieren und ausführen +status: todo + +Führe aus (in dieser Reihenfolge, da Apps voneinander abhängen): +```bash +uv run manage.py makemigrations accounts +uv run manage.py makemigrations core +uv run manage.py makemigrations ausschreibungen +uv run manage.py makemigrations lose +uv run manage.py makemigrations aufgaben +uv run manage.py makemigrations dokumente +uv run manage.py makemigrations preise +uv run manage.py makemigrations partner +uv run manage.py makemigrations bibliothek +uv run manage.py makemigrations marktbegleiter +uv run manage.py makemigrations nachbetrachtung +uv run manage.py makemigrations feedback +uv run manage.py migrate +``` + +Behebe etwaige Zirkulär-Import-Fehler durch String-Referenzen (z. B. +`FK('ausschreibungen.Ausschreibung', ...)` statt direktem Import). + +Prüfe: `uv run manage.py check` → keine Fehler. +Prüfe: `uv run manage.py showmigrations` → alle Migrationen als [X] markiert. +``` + +```task +id: WP-0002-T14 +title: Management-Command für Entwicklungs-Seed-Daten +status: todo + +Erstelle `vergabe_teilnahme/apps/core/management/commands/seed_dev.py` + +Der Command erstellt: +1. 3 Mitarbeiter (BM Max Muster, FV Anna Fach, GF Georg Chef) +2. 2 Dienstleistertypen (IT-Dienstleister, Rechtsberatung) +3. 1 Subunternehmer mit Zuordnung zu Dienstleistertyp +4. 2 Nachweise (ISO-27001 gültig, DSGVO-Muster gültig) +5. 1 Marktbegleiter +6. 3 Entscheidungsregeln (Muss-Anforderung nicht erfüllbar, Frist zu kurz, fehlende Referenz) +7. 1 Ausschreibung (Status 4, mit 2 Losen, 3 Anforderungen, 2 Aufgaben, 2 Preispunkten) + +Nutze `get_or_create` für alle Objekte damit der Command idempotent ist. +Ausgabe am Ende: Zusammenfassung der erstellten Objekte. + +Prüfe: `uv run manage.py seed_dev` läuft fehlerfrei durch. +Prüfe anschließend: `uv run manage.py shell -c "from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung; print(Ausschreibung.objects.count())"` → mindestens 1. +``` diff --git a/workplans/WP-0003-basis-ui.md b/workplans/WP-0003-basis-ui.md new file mode 100644 index 0000000..95365a7 --- /dev/null +++ b/workplans/WP-0003-basis-ui.md @@ -0,0 +1,502 @@ +--- +id: WP-0003 +title: Basis-UI — Shell-Layout, Templates, Template-Tags, Navigation +status: todo +phase: 3-of-12 +created: "2026-05-08" +depends_on: WP-0002 +--- + +# WP-0003 — Basis-UI + +Implementiert das Basis-Template-System: Shell-Layout mit Topbar und Sidebar, +alle Template-Tags (status_badge, phase_badge, render_field, flex_fields), +die globale und kontextuelle Navigation, den Feedback-Button und Error-Pages. + +**Arbeitsverzeichnis:** `/home/worsch/vergabe-teilnahme/` +**Templates-Pfad:** `vergabe_teilnahme/templates/` +**Template-Tags:** `vergabe_teilnahme/apps/core/templatetags/` + +Tailwind-Klassen aus WP-0001 (`static/src/main.css`) werden hier genutzt. + +--- + +```task +id: WP-0003-T01 +title: Template-Verzeichnisstruktur und base.html Shell-Layout +status: todo + +Erstelle Verzeichnisstruktur: +``` +vergabe_teilnahme/templates/ +├── base.html +├── partials/ +│ ├── topbar.html +│ ├── sidebar.html +│ ├── breadcrumb.html +│ ├── feedback_button.html +│ ├── feedback_modal.html +│ └── phase_nav.html +├── errors/ +│ ├── 404.html +│ └── 500.html +└── (app-spezifische Unterverzeichnisse folgen in späteren Workplans) +``` + +Füge in `settings/base.py` hinzu: +```python +TEMPLATES = [{ + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'vergabe_teilnahme' / 'templates'], + 'APP_DIRS': True, + ... +}] +``` + +`base.html` implementiert das Shell-Layout aus Blueprint Abschnitt 6.1: +```html + + + + + + {% block title %}Vergabe Teilnahme{% endblock %} + + + + + + {% include "partials/topbar.html" %} + +
+ + {% include "partials/sidebar.html" %} + + +
+ {% include "partials/breadcrumb.html" %} + {% block content %}{% endblock %} +
+
+ + + {% include "partials/feedback_button.html" %} + + + + + {% block extra_js %}{% endblock %} + + +``` +``` + +```task +id: WP-0003-T02 +title: Topbar-Partial +status: todo + +`vergabe_teilnahme/templates/partials/topbar.html`: + +Implementiere die Topbar mit: +- Logo / Appname "Vergabe Teilnahme" (links, Link zu /) +- Globale Suchleiste (Mitte): + ```html +
+ + +
+ ``` +- Avatar-Dropdown (rechts): Nutzername + Rolle, "Abmelden"-Link + (nutze Alpine.js x-show für Dropdown) + +Topbar-Höhe: `h-14` (56px), `bg-white border-b border-slate-200`. +``` + +```task +id: WP-0003-T03 +title: Sidebar globale Navigation +status: todo + +`vergabe_teilnahme/templates/partials/sidebar.html`: + +Implementiere die feste linke Sidebar (240px Breite) aus Blueprint Abschnitt 6.2. + +Struktur mit Alpine.js für aufklappbare Unterabschnitte: +```html + +``` + +Füge CSS-Hilfsklassen in `static/src/main.css` hinzu: +```css +.sidebar-link { @apply flex items-center px-3 py-2 rounded-lg text-sm text-slate-700 hover:bg-slate-100; } +.sidebar-link-active { @apply bg-brand-50 text-brand-700 font-medium; } +.sidebar-section-btn { @apply w-full flex items-center justify-between px-3 py-2 text-xs font-semibold text-slate-500 uppercase tracking-wide hover:text-slate-700; } +``` + +`current_ausschreibung` wird via Context-Processor bereitgestellt (nächster Task). +``` + +```task +id: WP-0003-T04 +title: Context-Processor und Phasen-Navigator-Partial +status: todo + +**Context-Processor** `vergabe_teilnahme/apps/core/context_processors.py`: +```python +def vergabe_context(request): + context = {} + # Aktueller Ausschreibungs-Kontext aus URL + ausschreibung_id = None + if hasattr(request, 'resolver_match') and request.resolver_match: + kwargs = request.resolver_match.kwargs + ausschreibung_id = kwargs.get('ausschreibung_id') or kwargs.get('pk') + if ausschreibung_id: + try: + from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung + context['current_ausschreibung'] = Ausschreibung.objects.get(pk=ausschreibung_id) + except (Ausschreibung.DoesNotExist, ValueError): + pass + return context +``` +Registriere in `settings/base.py` unter `TEMPLATES[0]['OPTIONS']['context_processors']`. + +**Phasen-Navigator** `partials/phase_nav.html`: +Zeigt die 8 Phasen als klickbare Links mit Statusindikator. +```html +
+

+ {{ current_ausschreibung.titel|truncatechars:30 }} +

+ {% for phase in phases %} + + + {{ phase.nummer }} + + {{ phase.name }} + {% if phase.warnung %}{% endif %} + + {% endfor %} +
+``` +Die `phases`-Liste wird von einer View-Hilfsfunktion `build_phase_nav(ausschreibung, current_url)` befüllt. +Implementiere diese Funktion in `core/services.py`. +``` + +```task +id: WP-0003-T05 +title: Breadcrumb-Partial +status: todo + +`vergabe_teilnahme/templates/partials/breadcrumb.html`: + +Breadcrumb rendert eine Liste von Links aus dem Template-Context-Variable `breadcrumbs`. +```html +{% if breadcrumbs %} + +{% endif %} +``` + +`breadcrumbs` wird in jeder View-Funktion als Liste von Dicts übergeben: +`[{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'}, {'label': 'Titel', 'url': None}]` + +Erstelle eine Hilfsfunktion `core.views_helpers.make_breadcrumbs(*args)` die diese Liste baut. +``` + +```task +id: WP-0003-T06 +title: Template-Tags: status_badge und phase_badge +status: todo + +Erstelle `vergabe_teilnahme/apps/core/templatetags/__init__.py` (leer). +Erstelle `vergabe_teilnahme/apps/core/templatetags/vergabe_tags.py`. + +```python +from django import template +register = template.Library() + +STATUS_COLORS = { + # Ausschreibung / Aufgabe / Dokument Status + 'offen': 'bg-slate-100 text-slate-700', + 'in_bearbeitung': 'bg-blue-100 text-blue-700', + 'erledigt': 'bg-green-100 text-green-700', + 'freigegeben': 'bg-green-100 text-green-700', + 'erteilt': 'bg-green-100 text-green-700', + 'gewonnen': 'bg-green-100 text-green-700', + 'ueberfaellig': 'bg-red-100 text-red-700', + 'nicht_erfuellbar': 'bg-red-100 text-red-700', + 'verloren': 'bg-red-100 text-red-700', + 'abgelehnt': 'bg-red-100 text-red-700', + 'ausstehend': 'bg-amber-100 text-amber-700', + 'in_pruefung': 'bg-amber-100 text-amber-700', + 'wartend_intern': 'bg-amber-100 text-amber-700', + 'wartend_sub': 'bg-amber-100 text-amber-700', + 'wartend_ausschreiber': 'bg-amber-100 text-amber-700', + 'archiviert': 'bg-gray-100 text-gray-500', + 'ersetzt': 'bg-gray-100 text-gray-500', + 'verworfen': 'bg-gray-100 text-gray-500', +} + +@register.inclusion_tag('partials/status_badge.html') +def status_badge(value, display_label=None): + css = STATUS_COLORS.get(value, 'bg-slate-100 text-slate-700') + return {'css': css, 'label': display_label or value.replace('_', ' ').capitalize()} + +@register.simple_tag +def phase_badge(nummer, zustand='todo'): + css_map = {'todo': 'phase-todo', 'active': 'phase-active', + 'done': 'phase-done', 'warn': 'phase-warn'} + return f'{nummer}' +``` + +Erstelle `partials/status_badge.html`: +```html + + {{ label }} + +``` +``` + +```task +id: WP-0003-T07 +title: Template-Tag: render_field (EntityFieldConfig-aware) +status: todo + +Ergänze `vergabe_tags.py`: + +```python +from vergabe_teilnahme.apps.core.models import EntityFieldConfig + +_HIDDEN_FIELDS_CACHE = {} + +def _is_field_hidden(entity_type, field_name): + key = (entity_type, field_name) + if key not in _HIDDEN_FIELDS_CACHE: + _HIDDEN_FIELDS_CACHE[key] = EntityFieldConfig.objects.filter( + entity_type=entity_type, field_name=field_name, is_hidden=True + ).exists() + return _HIDDEN_FIELDS_CACHE[key] + +def _get_field_label(entity_type, field_name, default_label): + try: + cfg = EntityFieldConfig.objects.get(entity_type=entity_type, field_name=field_name) + return cfg.display_label or default_label + except EntityFieldConfig.DoesNotExist: + return default_label + +@register.inclusion_tag('partials/field_row.html') +def render_field(obj, field_name, label=None, force_show=False): + entity_type = obj._meta.model_name + if not force_show and _is_field_hidden(entity_type, field_name): + return {'hidden': True} + value = getattr(obj, field_name, None) + display_label = _get_field_label(entity_type, field_name, label or field_name) + return {'hidden': False, 'label': display_label, 'value': value, 'field_name': field_name} +``` + +Erstelle `partials/field_row.html`: +```html +{% if not hidden %} +
+
{{ label }}
+
+ {% if value %}{{ value }}{% else %}{% endif %} +
+
+{% endif %} +``` + +Wichtig: `_HIDDEN_FIELDS_CACHE` per Request invalidieren (oder einfach kein Cache in v1, +da Admin-Änderungen sofort wirken sollen — entscheide dich für kein Caching in v1). +``` + +```task +id: WP-0003-T08 +title: Feedback-Button und Feedback-Modal-Partial +status: todo + +`partials/feedback_button.html`: +```html + +``` + +`partials/feedback_modal.html` (wird vom HTMX-Endpunkt zurückgegeben): +```html +
+
+

Feedback

+
+ {% csrf_token %} + + {% if current_ausschreibung %} + + {% endif %} +
+ +
+ + +
+
+ + +
+
+
+ + +
+
+
+
+``` + +Erstelle in `feedback/views.py`: +- `GET /feedback/modal/` → rendert `partials/feedback_modal.html` +- `POST /feedback/` → speichert Feedbackeintrag, gibt Danke-Fragment zurück +Verkable URLs in `feedback/urls.py` und include in Haupt-URLs. +``` + +```task +id: WP-0003-T09 +title: Error-Templates und Django-URL-Konfiguration +status: todo + +`vergabe_teilnahme/templates/errors/404.html`: +```html +{% extends "base.html" %} +{% block title %}Seite nicht gefunden{% endblock %} +{% block content %} +
+

404

+

Seite nicht gefunden

+

Die angeforderte Seite existiert nicht oder wurde verschoben.

+ Zur Übersicht +
+{% endblock %} +``` + +Analog `500.html`. + +In `settings/base.py`: +```python +handler404 = 'vergabe_teilnahme.apps.core.views.custom_404' +handler500 = 'vergabe_teilnahme.apps.core.views.custom_500' +``` + +In `core/views.py`: +```python +from django.shortcuts import render +def custom_404(request, exception=None): + return render(request, 'errors/404.html', status=404) +def custom_500(request): + return render(request, 'errors/500.html', status=500) +``` + +Füge alle App-URL-Dateien in `vergabe_teilnahme/urls.py` ein (auch wenn die Views +noch nicht existieren — mit leeren `urlpatterns = []` als Platzhalter): +```python +path('ausschreibungen/', include('vergabe_teilnahme.apps.ausschreibungen.urls')), +path('lose/', include('vergabe_teilnahme.apps.lose.urls')), +# ... alle Apps +``` +``` + +```task +id: WP-0003-T10 +title: Einfache Startseite mit Redirect und Smoke-Test +status: todo + +Erstelle `core/views.py` mit einer einfachen Redirect-View auf das Dashboard: +```python +from django.shortcuts import redirect +def home(request): + return redirect('ausschreibungen:dashboard') +``` + +Füge URL hinzu: `path('', core_views.home, name='home')` + +Erstelle eine minimale Dashboard-Placeholder-View in `ausschreibungen/views.py`: +```python +from django.shortcuts import render +def dashboard(request): + return render(request, 'ausschreibungen/dashboard.html', { + 'breadcrumbs': [{'label': 'Übersicht', 'url': None}] + }) +``` + +Erstelle `vergabe_teilnahme/templates/ausschreibungen/dashboard.html`: +```html +{% extends "base.html" %} +{% block title %}Übersicht{% endblock %} +{% block content %} +

Übersicht

+

Dashboard wird in WP-0004 implementiert.

+{% endblock %} +``` + +Verkable URL: `ausschreibungen/urls.py` → `path('', views.dashboard, name='dashboard')` +In Haupt-URLs: `path('', include('vergabe_teilnahme.apps.ausschreibungen.urls', namespace='ausschreibungen'))` + +Prüfe: `make dev` startet, `http://localhost:8000/` leitet auf Dashboard weiter, +Seite rendert ohne Template-Fehler. Sidebar und Topbar sind sichtbar. +``` diff --git a/workplans/WP-0004-dashboard-ausschreibungen.md b/workplans/WP-0004-dashboard-ausschreibungen.md new file mode 100644 index 0000000..d0a0417 --- /dev/null +++ b/workplans/WP-0004-dashboard-ausschreibungen.md @@ -0,0 +1,492 @@ +--- +id: WP-0004 +title: Dashboard und Ausschreibungen-CRUD +status: todo +phase: 4-of-12 +created: "2026-05-08" +depends_on: WP-0003 +--- + +# WP-0004 — Dashboard und Ausschreibungen-CRUD + +Vollständige Implementierung des Dashboards und aller Ausschreibungs-Views: +Liste, Suche/Filter, Anlegen, Detailseite, Status-Wechsel (HTMX), Teilnahmeentscheidung, +Entscheidungsregel-Auswertung, Archivierung und historische Erfassung. + +**Referenz:** UseCaseCatalog UC-OV-01 bis UC-OV-03, UC-AS-01 bis UC-AS-07. + +--- + +```task +id: WP-0004-T01 +title: Dashboard-View mit Kacheln und Fristenliste +status: todo + +`ausschreibungen/views.py` — Dashboard-View: +```python +def dashboard(request): + from vergabe_teilnahme.apps.core.services import get_deadline_warnings + from vergabe_teilnahme.apps.aufgaben.models import Aufgabe + from datetime import date, timedelta + + heute = date.today() + in_14_tagen = heute + timedelta(days=14) + + ctx = { + 'kritische_fristen': Ausschreibung.objects.filter( + abgabe_bis__date__lte=in_14_tagen, + abgabe_bis__date__gte=heute, + status__lt=10 + ).order_by('abgabe_bis')[:10], + + 'ohne_entscheidung': Ausschreibung.objects.filter( + status__in=[1, 2], + erstellt_am__lte=timezone.now() - timedelta(days=3) + ).order_by('erstellt_am')[:10], + + 'ueberfaellige_aufgaben': Aufgabe.objects.filter( + frist__lt=heute, + status__in=['offen', 'in_bearbeitung', 'wartend_intern', 'wartend_sub', 'wartend_ausschreiber'] + ).select_related('ausschreibung', 'verantwortlicher').order_by('frist')[:15], + + 'laufende_ausschreibungen': Ausschreibung.objects.filter( + status__range=(3, 9) + ).order_by('-geaendert_am')[:10], + + 'breadcrumbs': [{'label': 'Übersicht', 'url': None}], + } + return render(request, 'ausschreibungen/dashboard.html', ctx) +``` + +`ausschreibungen/dashboard.html` zeigt vier Kacheln-Zeilen: +Jede Kachel: Überschrift, Anzahl-Badge, Liste der Einträge mit Direktlinks. +Nutze `.card`-Klasse, `status_badge`-Tag und relative Fristangaben (z. B. "in 3 Tagen"). + +Ablaufende Nachweise: Nachweis-Modell aus Bibliothek mit `gueltig_bis ≤ heute + 60 Tage`. +``` + +```task +id: WP-0004-T02 +title: Ausschreibungsliste mit Filter und HTMX-Suche +status: todo + +`ausschreibungen/views.py` — ListView: +```python +def ausschreibung_liste(request): + qs = Ausschreibung.objects.all() + # Filter-Parameter + status = request.GET.get('status') + if status: + qs = qs.filter(status=status) + archiviert = request.GET.get('archiviert', '0') == '1' + qs = qs.filter(archiviert=archiviert) + verantwortlicher = request.GET.get('verantwortlicher') + if verantwortlicher: + qs = qs.filter(hauptverantwortung=verantwortlicher) + + qs = qs.select_related('hauptverantwortung').order_by('-geaendert_am') + ctx = { + 'ausschreibungen': qs, + 'status_choices': Ausschreibung.STATUS_CHOICES, + 'mitarbeiter': Mitarbeiter.objects.all(), + 'breadcrumbs': [{'label': 'Ausschreibungen', 'url': None}], + } + template = 'ausschreibungen/liste_partial.html' if request.htmx else 'ausschreibungen/liste.html' + return render(request, template, ctx) +``` + +`liste.html` — vollständige Seite mit Filterleiste oben und eingebetteter Tabelle. +`liste_partial.html` — nur die Tabellen-Rows (für HTMX-Filter-Update). + +Filterleiste: Dropdowns für Status, Verantwortlicher, Checkbox "Archivierte anzeigen". +Alle Filter-Änderungen: `hx-get="/ausschreibungen/" hx-target="#ausschreibungen-table" hx-push-url="true"`. + +Tabelle: Titel, Ausschreiber, Status (status_badge), Abgabefrist (farbig wenn < 14 Tage), +Verantwortlicher, Link zum Detail. +``` + +```task +id: WP-0004-T03 +title: Ausschreibung anlegen — Form und View (UC-AS-01) +status: todo + +`ausschreibungen/forms.py`: +```python +from django import forms + +class AusschreibungForm(forms.ModelForm): + class Meta: + model = Ausschreibung + fields = ['titel', 'ausschreiber', 'plattform', 'plattform_link', + 'ansprechpartner', 'hauptverantwortung', 'beschreibung', + 'strategische_relevanz', 'bieterfragen_bis', 'abgabe_bis', + 'zuschlag_bis', 'produktiv_bis'] + widgets = { + 'titel': forms.TextInput(attrs={'class': 'form-input', 'autofocus': True}), + 'ausschreiber': forms.TextInput(attrs={'class': 'form-input'}), + 'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}), + 'abgabe_bis': forms.DateTimeInput(attrs={'class': 'form-input', 'type': 'datetime-local'}), + 'bieterfragen_bis': forms.DateInput(attrs={'class': 'form-input', 'type': 'date'}), + # alle Datums-Widgets als type="date" + } +``` + +`ausschreibungen/views.py` — CreateView (function-based): +```python +def ausschreibung_neu(request): + if request.method == 'POST': + form = AusschreibungForm(request.POST) + if form.is_valid(): + a = form.save() + return redirect('ausschreibungen:detail', pk=a.pk) + else: + form = AusschreibungForm() + return render(request, 'ausschreibungen/form.html', { + 'form': form, + 'titel': 'Neue Ausschreibung', + 'breadcrumbs': [ + {'label': 'Ausschreibungen', 'url': '/ausschreibungen/'}, + {'label': 'Neu', 'url': None} + ], + }) +``` + +`ausschreibungen/form.html` — einfaches, gut gelayoutetes Formular. +Sections: Stammdaten, Fristen. Alle Felder nutzen `form-input` und `form-label`. +Submit: "Speichern" (btn-primary), "Abbrechen" (btn-ghost, zurück zur Liste). +``` + +```task +id: WP-0004-T04 +title: Ausschreibung-Detailseite (Phase 1 — Stammdaten) +status: todo + +`ausschreibungen/views.py` — Detailview: +```python +def ausschreibung_detail(request, pk): + a = get_object_or_404(Ausschreibung, pk=pk) + from vergabe_teilnahme.apps.core.services import get_deadline_warnings, build_phase_nav + ctx = { + 'ausschreibung': a, + 'ausschreibung_id': pk, # für Context-Processor / Phase-Navigator + 'warnungen': get_deadline_warnings(a), + 'freigaben': a.freigabe_set.all() if hasattr(a, 'freigabe_set') else [], + 'breadcrumbs': [ + {'label': 'Ausschreibungen', 'url': '/ausschreibungen/'}, + {'label': a.titel, 'url': None} + ], + } + return render(request, 'ausschreibungen/detail.html', ctx) +``` + +`ausschreibungen/detail.html`: +- Seitentitel: Ausschreibungstitel + Status-Badge + Edit-Button +- Warnungs-Banner (gelb/rot) falls `warnungen` nicht leer +- Abschnitt "Stammdaten": nutze `{% render_field ausschreibung "ausschreiber" "Ausschreiber" %}` für alle Felder +- Abschnitt "Fristen": alle Datums-Felder mit Restlaufzeit-Anzeige +- Abschnitt "Freigaben": kompakte Liste (typ, person, datum) +- Tab-Navigation zu Unterseiten (Lose, Anforderungen, Aufgaben, Bieterfragen, Preise, Abgabe, Nachbetrachtung) + als horizontale Link-Leiste unterhalb des Titels +- "Weitere Attribute" CustomAttribute-Panel (HTMX lazy-load, Implementierung in WP-0012) +``` + +```task +id: WP-0004-T05 +title: Ausschreibung bearbeiten (Edit-View) und Status inline wechseln +status: todo + +`ausschreibungen/views.py`: + +**Edit-View** (gleiche Form wie Neu, aber mit `instance=`): +```python +def ausschreibung_bearbeiten(request, pk): + a = get_object_or_404(Ausschreibung, pk=pk) + form = AusschreibungForm(request.POST or None, instance=a) + if request.method == 'POST' and form.is_valid(): + form.save() + return redirect('ausschreibungen:detail', pk=pk) + return render(request, 'ausschreibungen/form.html', {'form': form, 'titel': 'Bearbeiten', ...}) +``` + +**Status-Wechsel HTMX-Endpunkt:** +```python +def ausschreibung_status(request, pk): + a = get_object_or_404(Ausschreibung, pk=pk) + if request.method == 'POST': + neuer_status = int(request.POST.get('status', a.status)) + a.status = neuer_status + a.save(update_fields=['status', 'geaendert_am']) + return render(request, 'ausschreibungen/partials/status_widget.html', {'ausschreibung': a}) +``` + +`ausschreibungen/partials/status_widget.html`: +```html +
+ {% status_badge ausschreibung.get_status_display ausschreibung.status %} + +
+``` +``` + +```task +id: WP-0004-T06 +title: Teilnahmeentscheidung-Seite (Phase 2, UC-AS-04) +status: todo + +`ausschreibungen/views.py` — Teilnahmeentscheidungs-View: +```python +def ausschreibung_entscheidung(request, pk): + a = get_object_or_404(Ausschreibung, pk=pk) + if request.method == 'POST': + a.teilnahmeentscheidung = request.POST.get('teilnahmeentscheidung', 'offen') + a.beschreibung = request.POST.get('begruendung', a.beschreibung) + if a.teilnahmeentscheidung in ['teilnahme', 'ablehnung']: + a.status = max(a.status, 3) + a.save() + return redirect('ausschreibungen:detail', pk=pk) + + from vergabe_teilnahme.apps.ausschreibungen.services import entscheidungsregel_auswertung + ctx = { + 'ausschreibung': a, + 'regelergebnis': entscheidungsregel_auswertung(a), + 'ausschlusskriterien_nicht_erfuellbar': a.anforderung_set.filter( + ausschlusskriterium=True, erfuellungsstatus='nicht_erfuellbar' + ) if hasattr(a, 'anforderung_set') else [], + 'breadcrumbs': [...], + } + return render(request, 'ausschreibungen/entscheidung.html', ctx) +``` + +`ausschreibungen/entscheidung.html`: +- Zeigt offene Ausschlusskriterien als rote Warnmeldungen (wenn vorhanden) +- Zeigt Regelergebnis aus dem Katalog als strukturierte Liste +- Formular: Radio-Buttons für Teilnahme/Nichtteilnahme/Weitere Prüfung, Begründungsfeld +- "Freigabe erteilen"-Button (öffnet Freigabe-Modal, Implementierung in WP-0012) +``` + +```task +id: WP-0004-T07 +title: Entscheidungsregel-Auswertungs-Service +status: todo + +`vergabe_teilnahme/apps/ausschreibungen/services.py`: + +```python +def entscheidungsregel_auswertung(ausschreibung): + """ + Wendet alle aktiven Entscheidungsregeln auf eine Ausschreibung an. + Gibt Liste von Ergebnis-Dicts zurück. + """ + from vergabe_teilnahme.apps.bibliothek.models import Entscheidungsregel + regeln = Entscheidungsregel.objects.filter(aktiv=True).order_by('-gewichtung') + ergebnisse = [] + for regel in regeln: + ergebnis = _wende_regel_an(regel, ausschreibung) + ergebnisse.append({ + 'regel': regel, + 'empfehlung': ergebnis['empfehlung'], + 'begruendung': ergebnis['begruendung'], + 'warnung': ergebnis['empfehlung'] == 'nicht_teilnehmen', + }) + return ergebnisse + +def _wende_regel_an(regel, ausschreibung): + """ + Einfache Heuristik für v1: Überprüft bekannte Regel-Kategorien. + Für unbekannte Kategorien: gibt neutrale Empfehlung zurück. + """ + kat = regel.kategorie + if kat == 'ausschlusskriterium' and hasattr(ausschreibung, 'anforderung_set'): + hat_ausschluss = ausschreibung.anforderung_set.filter( + ausschlusskriterium=True, erfuellungsstatus='nicht_erfuellbar' + ).exists() + if hat_ausschluss: + return {'empfehlung': 'nicht_teilnehmen', + 'begruendung': 'Nicht erfüllbares Ausschlusskriterium vorhanden.'} + if kat == 'frist' and ausschreibung.abgabe_bis: + from datetime import date + delta = (ausschreibung.abgabe_bis.date() - date.today()).days + if regel.schwellenwert and delta < regel.schwellenwert: + return {'empfehlung': 'nicht_teilnehmen', + 'begruendung': f'Restlaufzeit {delta} Tage unter Schwellenwert.'} + return {'empfehlung': 'pruefen', 'begruendung': regel.begruendung or '—'} +``` +``` + +```task +id: WP-0004-T08 +title: Ausschreibung archivieren und historisch erfassen (UC-AS-06, UC-AS-07) +status: todo + +**Archivieren:** +```python +def ausschreibung_archivieren(request, pk): + a = get_object_or_404(Ausschreibung, pk=pk) + if request.method == 'POST': + a.archiviert = True + a.status = 13 + a.save(update_fields=['archiviert', 'status', 'geaendert_am']) + return redirect('ausschreibungen:liste') + return render(request, 'ausschreibungen/archivieren_confirm.html', {'ausschreibung': a}) +``` + +`archivieren_confirm.html`: Einfacher Bestätigungsdialog (Alpine.js Modal oder eigene Seite). + +**Historisch erfassen:** +Ergänze `AusschreibungForm` um ein BooleanField `historisch_erfassen` (Widget: HiddenInput). +Bei `historisch_erfassen=True` zeigt das Formular zusätzlich die Felder: +`ergebnis`, `teilnahmeentscheidung` — direkt befüllbar ohne Phasenreihenfolge. +Die Detailseite wird nach dem Speichern sofort mit allen Unterseiten (Preise, Nachbetrachtung etc.) +zugänglich — keine Einschränkung. + +URL für historische Erfassung: `/ausschreibungen/neu/?historisch=1` +Die View prüft diesen Parameter und setzt `historisch_erfassen` im initialen Form-Context. +``` + +```task +id: WP-0004-T09 +title: Globale Suchleiste — HTMX-Endpunkt und Ergebnis-Template +status: todo + +`core/views.py`: +```python +def global_search(request): + q = request.GET.get('q', '').strip() + if len(q) < 2: + return HttpResponse('') + ctx = { + 'q': q, + 'ausschreibungen': Ausschreibung.objects.filter( + Q(titel__icontains=q) | Q(ausschreiber__icontains=q) + )[:5], + 'aufgaben': Aufgabe.objects.filter(titel__icontains=q)[:5], + 'subunternehmer': Subunternehmer.objects.filter(name__icontains=q)[:5], + 'marktbegleiter': Marktbegleiter.objects.filter(name__icontains=q)[:3], + } + return render(request, 'partials/search_results.html', ctx) +``` + +`partials/search_results.html`: +```html +{% if ausschreibungen or aufgaben or subunternehmer %} +
+ {% if ausschreibungen %} +
+

Ausschreibungen

+ {% for a in ausschreibungen %} + + {{ a.titel }} — {{ a.ausschreiber }} + + {% endfor %} +
+ {% endif %} + +
+{% endif %} +``` + +URL: `path('suche/', core_views.global_search, name='global_search')` +Topbar-Formular (aus WP-0003-T02) zeigt Ergebnisse in `#search-results`. +``` + +```task +id: WP-0004-T10 +title: Ausschreibungen-URL-Verkabelung und App-Namespace +status: todo + +`vergabe_teilnahme/apps/ausschreibungen/urls.py`: +```python +from django.urls import path +from . import views + +app_name = 'ausschreibungen' + +urlpatterns = [ + path('', views.ausschreibung_liste, name='liste'), + path('neu/', views.ausschreibung_neu, name='neu'), + path('/', views.ausschreibung_detail, name='detail'), + path('/bearbeiten/', views.ausschreibung_bearbeiten, name='bearbeiten'), + path('/status/', views.ausschreibung_status, name='status'), + path('/entscheidung/', views.ausschreibung_entscheidung, name='entscheidung'), + path('/archivieren/', views.ausschreibung_archivieren, name='archivieren'), + # Unterseiten-URLs (Platzhalter für spätere Workplans): + path('/lose/', include('vergabe_teilnahme.apps.lose.urls')), + path('/aufgaben/', include('vergabe_teilnahme.apps.aufgaben.urls')), + path('/bieterfragen/', include('vergabe_teilnahme.apps.aufgaben.bieterfragen_urls')), + path('/dokumente/', include('vergabe_teilnahme.apps.dokumente.urls')), + path('/preise/', include('vergabe_teilnahme.apps.preise.urls')), + path('/abgabe/', include('vergabe_teilnahme.apps.nachbetrachtung.abgabe_urls')), + path('/nachbetrachtung/', include('vergabe_teilnahme.apps.nachbetrachtung.urls')), + path('/marktbegleiter/', include('vergabe_teilnahme.apps.marktbegleiter.passagen_urls')), +] +``` + +Jede referenzierte App-URL-Datei wird hier als leere Stub-Datei angelegt +(`urlpatterns = []`) damit die includes nicht zu ImportErrors führen. + +Prüfe: `uv run manage.py check --deploy` → keine URL-Fehler. +Smoke-Test: alle Hauptseiten (/ausschreibungen/, /ausschreibungen/neu/) laden ohne 500. +``` + +```task +id: WP-0004-T11 +title: Ausschreibungs-Tests (Models und Views) +status: todo + +Erstelle `vergabe_teilnahme/apps/ausschreibungen/tests/`: + +`test_models.py`: +- Test: Ausschreibung `__str__` gibt Titel zurück +- Test: `ist_aktiv` property für Status 1-9 (True) und 10-13 (False) +- Test: `naechste_frist` gibt das frühere von bieterfragen_bis/abgabe_bis zurück + +`test_views.py` (nutze `pytest-django` + `client` fixture): +- Test: GET /ausschreibungen/ → 200 +- Test: GET /ausschreibungen/neu/ → 200 +- Test: POST /ausschreibungen/neu/ mit validen Daten → Redirect zur Detailseite +- Test: GET /ausschreibungen// → 200 +- Test: POST /ausschreibungen//status/ mit status=4 → 200, Ausschreibung hat status=4 +- Test: Status-Wechsel mit HTMX-Header → partial template response + +Nutze `factory_boy` für Factories: +```python +import factory +class AusschreibungFactory(factory.django.DjangoModelFactory): + class Meta: + model = Ausschreibung + titel = factory.Sequence(lambda n: f"Ausschreibung {n}") + ausschreiber = "Testausschreiber GmbH" + status = 1 +``` +``` + +```task +id: WP-0004-T12 +title: Seed-Daten prüfen und Dashboard-Kacheln verifizieren +status: todo + +Führe die gesamte Integrations-Smoke-Test-Sequenz durch: + +1. `make db` → PostgreSQL läuft +2. `uv run manage.py migrate` → alle Migrationen sauber +3. `uv run manage.py seed_dev` → Seed-Daten angelegt +4. `make dev` → Server läuft +5. Browser öffnen: `http://localhost:8000/` + → Dashboard zeigt Kacheln (auch wenn leer) + → Sidebar zeigt alle globalen Navpunkte + → Topbar mit Suchleiste sichtbar +6. `http://localhost:8000/ausschreibungen/` + → Liste zeigt die Seed-Ausschreibung +7. Ausschreibung öffnen → Detail-Seite rendert mit Stammdaten +8. Status-Dropdown wechseln → HTMX aktualisiert Status inline +9. `http://localhost:8000/ausschreibungen/neu/` → Formular funktioniert +10. `uv run pytest vergabe_teilnahme/apps/ausschreibungen/` → alle Tests grün + +Erst wenn alle 10 Punkte erfüllt sind: Task als done markieren. +``` diff --git a/workplans/WP-0005-lose-anforderungen.md b/workplans/WP-0005-lose-anforderungen.md new file mode 100644 index 0000000..7fc3f89 --- /dev/null +++ b/workplans/WP-0005-lose-anforderungen.md @@ -0,0 +1,211 @@ +--- +id: WP-0005 +title: Lose und Anforderungen +status: todo +phase: 5-of-12 +created: "2026-05-08" +depends_on: WP-0004 +--- + +# WP-0005 — Lose und Anforderungen + +Implementiert alle Views, Forms und Templates für Lose (UC-LA-01) und Anforderungen +(UC-LA-02 bis UC-LA-05) inklusive Nachweis-Verknüpfung und Ausschlusskriterium-Eskalation. + +**URL-Präfix:** `/ausschreibungen//lose/` und `.../anforderungen/` + +--- + +```task +id: WP-0005-T01 +title: Lose-Liste und Lose anlegen (UC-LA-01) +status: todo + +`lose/views.py` — lose_liste und los_neu: + +lose_liste: Zeigt alle Lose einer Ausschreibung geordnet nach Losnummer. +Template: `lose/liste.html` — Tabelle mit Losnummer, Lostitel, Zuständiger, Teilnahme (Badge), Status. +"+ Los hinzufügen"-Button öffnet Inline-Formular via HTMX. + +`LosForm(ModelForm)`: Felder losnummer, lostitel, beschreibung, abgrenzung, zustaendiger, teilnahme. +Alle Inputs mit `form-input`, Textarea mit 3 rows. + +los_neu (POST): Erstellt Los, gibt bei HTMX-Request nur neuen Tabellen-Row zurück. +los_bearbeiten (GET/POST): Edit in eigenem Template oder Inline. +los_loeschen (POST): Löscht Los nach Bestätigung. + +URLs in `lose/urls.py`: +```python +path('', views.lose_liste, name='liste'), +path('neu/', views.los_neu, name='neu'), +path('/', views.los_detail, name='detail'), +path('/bearbeiten/', views.los_bearbeiten, name='bearbeiten'), +``` +``` + +```task +id: WP-0005-T02 +title: Los-Detail-Seite mit eingebetteten Anforderungen +status: todo + +`lose/views.py` — los_detail: +```python +def los_detail(request, ausschreibung_id, los_pk): + los = get_object_or_404(Los, pk=los_pk, ausschreibung_id=ausschreibung_id) + ctx = { + 'los': los, + 'ausschreibung': los.ausschreibung, + 'anforderungen': los.anforderungen.all().order_by('verbindlichkeit', 'titel'), + 'subunternehmer': SubunternehmerZuordnung.objects.filter(los=los), + ... + } + return render(request, 'lose/detail.html', ctx) +``` + +`lose/detail.html`: +- Los-Stammdaten (render_field Tags) +- Abschnitt "Anforderungen" — Tabelle mit Link zur Anforderungsdetailseite +- Abschnitt "Subunternehmer" — Liste zugeordneter Subunternehmer +- CustomAttribute-Panel (HTMX lazy, wie in WP-0012) +- Abschnitt "Teilnahme": Toggle Ja/Nein/Offen (HTMX POST) +``` + +```task +id: WP-0005-T03 +title: Anforderungsliste nach Los gruppiert (UC-LA-02) +status: todo + +`lose/views.py` — anforderungen_liste: +Lädt alle Anforderungen der Ausschreibung, gruppiert nach Los. +Anforderungen ohne Los-Zuordnung erscheinen in "Allgemein". + +Template `lose/anforderungen_liste.html`: +- Filter-Leiste: Verbindlichkeit (Muss/Soll/Kann), Erfüllungsstatus, Los, Zuständiger +- Alle Filteränderungen via HTMX ohne Reload +- Pro Gruppe: aufklappbarer Akkordeon-Abschnitt (Alpine.js x-show) +- Jede Zeile: Titel, Verbindlichkeit-Badge, Erfüllungsstatus-Badge, Zuständiger +- Rote Hervorhebung bei Ausschlusskriterium + nicht_erfuellbar +- "+ Anforderung" Button oben rechts +``` + +```task +id: WP-0005-T04 +title: Anforderung anlegen und Detailseite (UC-LA-02, UC-LA-03) +status: todo + +`AnforderungForm(ModelForm)`: alle Felder aus Modell. +Besonderer Widget für verbindlichkeit: Radio-Buttons statt Dropdown. + +anforderung_neu: Liest ausschreibung_id aus URL. Los-Dropdown zeigt nur Lose der aktuellen Ausschreibung. + +anforderung_detail: Zeigt alle Felder (via render_field), Kommentarverlauf als Timeline +(für v1: einfache Textarea + gespeicherte Kommentare als JSONField auf Anforderung). +Verknüpfte Dokumente, Nachweise und Aufgaben als Listen. + +Erfüllungsstatus inline ändern (UC-LA-03): +HTMX-Endpunkt `anforderung_status`: +```python +def anforderung_status(request, ausschreibung_id, pk): + a = get_object_or_404(Anforderung, pk=pk, ausschreibung_id=ausschreibung_id) + if request.method == 'POST': + a.erfuellungsstatus = request.POST['erfuellungsstatus'] + a.save(update_fields=['erfuellungsstatus']) + return render(request, 'lose/partials/erfuellungsstatus_widget.html', {'anforderung': a}) +``` +``` + +```task +id: WP-0005-T05 +title: Nachweis-Verknüpfung mit Bibliothek (UC-LA-04) +status: todo + +`lose/views.py` — nachweis_suche_modal und nachweis_zuordnen: + +Auf der Anforderungsdetailseite: Button "Nachweis zuordnen". +```html + +
+``` + +nachweis_suche_modal (GET): Gibt Such-Modal zurück mit Textfeld + Ergebnisliste (HTMX-Suche im Bibliothek-Bestand). +Jeder Treffer zeigt: Titel, Ablaufdatum, Freigabestatus. Ablaufende/abgelaufene Nachweise in Orange/Rot. + +nachweis_zuordnen (POST): Fügt Nachweis via M2M hinzu. +nachweis_entfernen (DELETE/POST): Entfernt M2M-Verknüpfung. + +Zeige zugeordnete Nachweise auf Anforderungsdetail als Liste mit Ablaufstatus-Badge. +``` + +```task +id: WP-0005-T06 +title: Ausschlusskriterium-Eskalation auf Phase-2-Seite (UC-LA-05) +status: todo + +Ergänze `ausschreibungen/views.py` — ausschreibung_entscheidung: + +Vor dem Laden der Seite: Prüfe ob es Anforderungen mit +`ausschlusskriterium=True AND erfuellungsstatus='nicht_erfuellbar'` gibt. + +Falls ja: Zeige oben auf der Entscheidungsseite einen roten Alert-Banner: +```html +{% if ausschlusskriterien_nicht_erfuellbar %} +
+

⚠ Nicht erfüllbare Ausschlusskriterien

+
    + {% for a in ausschlusskriterien_nicht_erfuellbar %} +
  • {{ a.titel }} (Los: {{ a.los|default:"Allgemein" }})
  • + {% endfor %} +
+

Empfehlung: Nichtteilnahme

+
+{% endif %} +``` + +Prüfe: Seed-Daten mit einer nicht erfüllbaren Muss-Anforderung + Ausschlusskriterium anlegen, +dann Phase-2-Seite öffnen → Banner erscheint. +``` + +```task +id: WP-0005-T07 +title: Aufgabe aus Anforderung ableiten (UC-AU-02) +status: todo + +Auf der Anforderungsdetailseite: Button "Aufgabe erstellen". +```python +def anforderung_aufgabe_erstellen(request, ausschreibung_id, pk): + anforderung = get_object_or_404(Anforderung, pk=pk) + if request.method == 'POST': + from vergabe_teilnahme.apps.aufgaben.models import Aufgabe + Aufgabe.objects.create( + ausschreibung_id=ausschreibung_id, + los=anforderung.los, + anforderung=anforderung, + titel=f"Klärung: {anforderung.titel[:200]}", + typ='fachlich', + verantwortlicher=anforderung.zustaendiger, + ) + return redirect('ausschreibungen:lose:anforderung_detail', + ausschreibung_id=ausschreibung_id, pk=pk) + return render(request, 'lose/aufgabe_erstellen_confirm.html', {'anforderung': anforderung}) +``` + +Nach Erstellen: Anforderungsdetail zeigt die neue Aufgabe im Abschnitt "Verbundene Aufgaben". +``` + +```task +id: WP-0005-T08 +title: Tests für Lose und Anforderungen +status: todo + +`lose/tests/test_views.py`: +- Test: Lose-Liste gibt 200 zurück +- Test: Los anlegen mit POST → Redirect, Los existiert in DB +- Test: Anforderung anlegen mit Muss-Verbindlichkeit +- Test: Erfüllungsstatus via HTMX-POST auf 'nicht_erfuellbar' setzen +- Test: Ausschlusskriterium + nicht_erfuellbar → Entscheidungsseite zeigt Alert-Banner +- Test: Nachweis-Verknüpfung über M2M + +Nutze AusschreibungFactory und erstelle Factories für Los und Anforderung. +``` diff --git a/workplans/WP-0006-aufgaben-bieterfragen.md b/workplans/WP-0006-aufgaben-bieterfragen.md new file mode 100644 index 0000000..75bf65e --- /dev/null +++ b/workplans/WP-0006-aufgaben-bieterfragen.md @@ -0,0 +1,150 @@ +--- +id: WP-0006 +title: Aufgaben und Bieterfragen +status: todo +phase: 6-of-12 +created: "2026-05-08" +depends_on: WP-0005 +--- + +# WP-0006 — Aufgaben und Bieterfragen + +Implementiert alle Views für Aufgaben (UC-AU-01 bis UC-AU-04) und Bieterfragen +(UC-BF-01 bis UC-BF-03) inklusive globaler Aufgabenliste und Fristwarnung. + +--- + +```task +id: WP-0006-T01 +title: Aufgabenliste pro Ausschreibung und globale Liste (UC-OV-03, UC-AU-01) +status: todo + +`aufgaben/views.py` — aufgaben_liste: + +**Pro Ausschreibung** (`/ausschreibungen//aufgaben/`): +Zeigt alle Aufgaben dieser Ausschreibung, filterbar nach Status, Typ, Verantwortlicher. +Template `aufgaben/liste.html` — Tabelle mit: Titel, Typ-Badge, Priorität, Frist (rot wenn überfällig), +Verantwortlicher, Status-Badge, Inline-Status-Dropdown. + +**Globale Liste** (`/aufgaben/`): +Gleiche View mit `ausschreibung_id=None`. Zusätzlicher Filter: "Nur meine Aufgaben" +(request.user als Verantwortlicher). +Zeigt zusätzliche Spalte "Ausschreibung" mit Link. + +`Aufgabe.objects.filter(frist__lt=today, status__in=AKTIVE_STATUS)`: +Vor dem Rendering: Update alle überfälligen Aufgaben via `update()` auf status='ueberfaellig'. +(Kein Celery nötig — wird beim Seitenaufruf der Liste getriggert.) + +URL in `aufgaben/urls.py`: +```python +path('', views.aufgaben_liste, name='liste'), +``` +Globale URL in Haupt-urls.py: `path('aufgaben/', include('vergabe_teilnahme.apps.aufgaben.urls'))` +``` + +```task +id: WP-0006-T02 +title: Aufgabe anlegen und zuweisen (UC-AU-01) +status: todo + +`AufgabeForm(ModelForm)`: +- `typ` als Select, `prioritaet` als Radio (Hoch/Mittel/Niedrig) +- `frist` als DateInput type="date" +- `los`, `anforderung`, `bieterfrage` als optionale Selects (gefiltert auf aktuelle Ausschreibung) + +`aufgabe_neu (POST)`: Bei HTMX-Request gibt es die neue Tabellenzeile zurück +(kein Full-Page-Reload). Sonst Redirect zu Aufgabenliste. + +`aufgabe_bearbeiten`: Gleiche Form mit `instance`. +`aufgabe_loeschen (POST)`: Setzt status='verworfen' statt hartem Delete. +`aufgabe_detail`: Zeigt alle Felder, verknüpfte Anforderung/Bieterfrage, Ergebnisfeld. +``` + +```task +id: WP-0006-T03 +title: Aufgabenstatus inline ändern und Ergebnis dokumentieren (UC-AU-03) +status: todo + +**Status-Widget** (analog zum Ausschreibungs-Status-Widget): +Jede Zeile in der Aufgabenliste enthält ein Status-Dropdown: +```html + +``` + +Bei Status-Wechsel auf 'erledigt': HTMX-Response zeigt zusätzlich ein Ergebnis-Eingabefeld inline. +Nutzer kann Ergebnis eintragen und separat abspeichern. + +`aufgabe_status (POST)`: Aktualisiert Status, gibt einzelne Tabellenzeile zurück. +`aufgabe_ergebnis (POST)`: Speichert Ergebnistext. +``` + +```task +id: WP-0006-T04 +title: Bieterfragen-Liste und Bieterfrage anlegen (UC-BF-01, UC-BF-02) +status: todo + +`aufgaben/views.py` — bieterfragen_liste und bieterfrage_neu: + +Template `aufgaben/bieterfragen_liste.html`: +- Fristwarnung oben: "Bieterfragen bis: — noch X Tage" (roter/gelber Banner) +- Filter: Status, Priorität, Verantwortlicher +- Tabelle: Frage (gekürzt), Status-Badge, Priorität, Einreichungsdatum, Verantwortlicher + +`BieterfragenForm(ModelForm)`: +`frage` als Textarea, `hintergrund` als Textarea, +`anforderung` + `dokument` als optionale Selects. + +bieterfrage_neu: Kann aus Anforderung vorausgefüllt werden (GET-Parameter `anforderung_id`). +bieterfrage_detail: Zeigt Frage, Hintergrund, verknüpfte Anforderung, Status-Timeline. + +URLs in `aufgaben/bieterfragen_urls.py`: +```python +path('', views.bieterfragen_liste, name='liste'), +path('neu/', views.bieterfrage_neu, name='neu'), +path('/', views.bieterfrage_detail, name='detail'), +path('/status/', views.bieterfrage_status, name='status'), +path('/antwort/', views.bieterfrage_antwort, name='antwort'), +``` +``` + +```task +id: WP-0006-T05 +title: Bieterfragen-Workflow und Antwort einarbeiten (UC-BF-03) +status: todo + +`bieterfrage_status (POST)`: Ermöglicht Status-Wechsel über definierte Übergänge: +entwurf → abgestimmt → eingereicht → beantwortet → eingearbeitet. +Beim Wechsel auf 'eingereicht': Setzt einreichungsdatum=heute (wenn noch nicht gesetzt). + +`bieterfrage_antwort (POST)`: +Speichert `antwort`, `auswirkung_angebot`. +Falls eine Anforderung verknüpft ist: Zeigt Button "Anforderungsstatus aktualisieren" → +Weiterleitung zur Anforderungsdetailseite mit vorausgefülltem Erfüllungsstatus. + +Auf der Bieterfragen-Detailseite: +- Status-Timeline als vertikale Stepper-Darstellung (CSS-only mit Tailwind) +- Antwortformular (erscheint nur bei Status 'eingereicht'/'beantwortet') +- "Einarbeiten"-Checkbox setzt Status direkt auf 'eingearbeitet' und `eingearbeitet=True` +``` + +```task +id: WP-0006-T06 +title: Aufgaben- und Bieterfragen-Tests +status: todo + +`aufgaben/tests/test_views.py`: +- Test: Aufgabenliste gibt 200 zurück +- Test: Neue Aufgabe anlegen → erscheint in Liste +- Test: Status-Wechsel via HTMX → Zeile enthält neuen Status-Badge +- Test: Überfällige Aufgabe → status wird auf 'ueberfaellig' gesetzt bei Listenabruf +- Test: Bieterfrage aus Anforderung vorausgefüllt (GET mit anforderung_id) +- Test: Antwort speichern → antwort-Feld befüllt, Status auf 'beantwortet' +``` diff --git a/workplans/WP-0007-dokumente.md b/workplans/WP-0007-dokumente.md new file mode 100644 index 0000000..45d8392 --- /dev/null +++ b/workplans/WP-0007-dokumente.md @@ -0,0 +1,157 @@ +--- +id: WP-0007 +title: Dokumentenmanagement +status: todo +phase: 7-of-12 +created: "2026-05-08" +depends_on: WP-0006 +--- + +# WP-0007 — Dokumentenmanagement + +Upload, Kategorisierung, Versionierung, Statusworkflow und Standarddokument-Zuordnung +für alle Dokumente. Referenz: UC-DO-01 bis UC-DO-05. + +--- + +```task +id: WP-0007-T01 +title: Dokument-Upload und Kategorisierung (UC-DO-01) +status: todo + +`dokumente/views.py` — dokument_upload: + +`DokumentForm(ModelForm)`: +Felder: datei (FileInput), kategorie (Select), version (Text, default='1.0'), +quelle (Text, blank), verantwortlicher (Select), pruefer (Select, blank), los (Select, blank). +`clean_datei()` prüft Dateiendung (.pdf, .docx, .xlsx, .zip, .png, .jpg, .jpeg) und +Dateigröße ≤ settings.MAX_UPLOAD_SIZE. Fehler: ValidationError mit klarer Meldung. + +Nach erfolgreichem Upload: +- `dateiname` wird aus `datei.name` befüllt (`os.path.basename(form.instance.datei.name)`) +- Redirect zur Dokumentenliste der Ausschreibung + +Multi-Upload: Zeige Dropzone (``) mit Alpine.js-Preview der +gewählten Dateien (Dateinamen-Liste). Für jede Datei eigenes Formular-Submit +(vereinfacht: ein File at a time in v1). +``` + +```task +id: WP-0007-T02 +title: Dokumentenliste und Dokumentdetail +status: todo + +`dokumente/views.py` — dokumente_liste: +Zeigt alle Dokumente einer Ausschreibung, gruppiert nach Kategorie. +Filter: Status, Kategorie, Verantwortlicher. +Template `dokumente/liste.html`: +- Akkordeon nach Kategorie (Alpine.js) +- Tabelle: Dateiname (Download-Link), Version, Status-Badge, Verantwortlicher, Prüfer, Datum +- Rote Markierung für `finale_abgabeversion=True` + +`dokument_detail`: +- Alle Felder via render_field +- Download-Button für Datei +- Versionshistorie (alle Dokumente gleicher Kategorie + Name, geordnet nach Version) +- Freigaben-Liste via GenericRelation +- CustomAttribute-Panel +``` + +```task +id: WP-0007-T03 +title: Neue Dokumentversion hochladen (UC-DO-02) +status: todo + +`dokument_neue_version (POST)`: +```python +def dokument_neue_version(request, ausschreibung_id, pk): + altes_dokument = get_object_or_404(Dokument, pk=pk, ausschreibung_id=ausschreibung_id) + form = DokumentVersionForm(request.POST, request.FILES) + if form.is_valid(): + neues_dok = form.save(commit=False) + neues_dok.ausschreibung = altes_dokument.ausschreibung + neues_dok.los = altes_dokument.los + neues_dok.kategorie = altes_dokument.kategorie + neues_dok.verantwortlicher = altes_dokument.verantwortlicher + neues_dok.save() + altes_dokument.status = 'ersetzt' + altes_dokument.save(update_fields=['status']) + return redirect('dokumente:detail', ausschreibung_id=ausschreibung_id, pk=neues_dok.pk) + return render(request, 'dokumente/neue_version.html', {'form': form, 'dokument': altes_dokument}) +``` + +`DokumentVersionForm`: Nur datei + version (vorausgefüllt mit inkrementierter Versionsnummer). +Logik `naechste_version(alte_version_str)`: "1.0" → "2.0", "2.3" → "3.0" (Major-Inkrement für neue Versionen). +``` + +```task +id: WP-0007-T04 +title: Dokumentstatus-Workflow und finale Abgabeversion (UC-DO-03, UC-DO-04) +status: todo + +**Status-Workflow** — HTMX-Widget analog zum Aufgaben-Status. +Statusübergänge: hochgeladen → zu_pruefen → in_bearbeitung → geprueft → freigegeben → final_abgegeben. + +`dokument_status (POST)`: +Bei Übergang auf 'final_abgegeben': Setze automatisch `finale_abgabeversion=True`. +Bei Übergang auf 'final_abgegeben': Sperre weitere Status-Änderungen +(Widget rendert dann nur readonly Status-Badge ohne Dropdown). + +**Finale Abgabeversion kennzeichnen** (UC-DO-04): +Zusätzlicher Button "Als finale Abgabeversion kennzeichnen" (außerhalb des normalen Workflows): +`dokument_finale_version (POST)`: +```python +def dokument_finale_version(request, ausschreibung_id, pk): + dok = get_object_or_404(Dokument, pk=pk) + dok.finale_abgabeversion = True + dok.status = 'final_abgegeben' + dok.save(update_fields=['finale_abgabeversion', 'status']) + return render(request, 'dokumente/partials/finaler_status_badge.html', {'dokument': dok}) +``` +Nach Kennzeichnung erscheint grüner "Final" Badge; weitere Uploads zu dieser Version gesperrt. +``` + +```task +id: WP-0007-T05 +title: Standarddokument aus Bibliothek zuordnen (UC-DO-05) +status: todo + +`dokument_bibliothek_zuordnen`: +HTMX-Modal mit Suchfeld. Suche in `bibliothek.Nachweis` und Bibliothek-Dokumente. +Jeder Treffer zeigt: Titel, Kategorie, Version, Ablaufdatum, Freigabestatus. + +Zuordnung erstellt **keinen** neuen Upload, sondern einen Dokument-Datensatz mit: +- `datei` = leer (null) +- `quelle` = "Bibliothek: " +- `dateiname` = Nachweis-Titel +- Referenz auf Nachweis via FK (`bibliothek_nachweis` FK(Nachweis, null=True, SET_NULL) — ergänze Feld im Dokument-Modell + Migration) + +Ablaufende/abgelaufene Nachweise: Zeige Warnung in orange/rot. +``` + +```task +id: WP-0007-T06 +title: Dokument-URL-Verkabelung und Tests +status: todo + +`dokumente/urls.py`: +```python +app_name = 'dokumente' +urlpatterns = [ + path('', views.dokumente_liste, name='liste'), + path('hochladen/', views.dokument_upload, name='upload'), + path('/', views.dokument_detail, name='detail'), + path('/version/', views.dokument_neue_version, name='neue_version'), + path('/status/', views.dokument_status, name='status'), + path('/final/', views.dokument_finale_version, name='finale_version'), + path('/bibliothek/', views.dokument_bibliothek_zuordnen, name='bibliothek_zuordnen'), +] +``` + +Tests: +- Test: Upload mit gültigem PDF → Dokument in DB, Datei im Dateisystem +- Test: Upload mit ungültiger Dateierweiterung → ValidationError +- Test: Upload zu groß → ValidationError +- Test: Neue Version hochladen → altes Dokument hat status='ersetzt' +- Test: Finale Abgabeversion → finale_abgabeversion=True, Status gesperrt +``` diff --git a/workplans/WP-0008-preise.md b/workplans/WP-0008-preise.md new file mode 100644 index 0000000..b50f7e7 --- /dev/null +++ b/workplans/WP-0008-preise.md @@ -0,0 +1,160 @@ +--- +id: WP-0008 +title: Preise und Marktpreisauswertung +status: todo +phase: 8-of-12 +created: "2026-05-08" +depends_on: WP-0007 +--- + +# WP-0008 — Preise und Marktpreisauswertung + +Preispunkt-CRUD, Vergleichsgewicht-Validierung, gewichtete Durchschnittsberechnung +und Leistungstyp-Auswertung. Referenz: UC-PR-01 bis UC-PR-04, FR-25 bis FR-36. + +--- + +```task +id: WP-0008-T01 +title: Preispunkt anlegen mit Vergleichsgewicht-Validierung (UC-PR-01, UC-PR-02) +status: todo + +`preise/views.py` — preispunkt_neu: + +`PreispunktForm(ModelForm)`: +- Felder: leistungstyp, konkrete_leistung, mengeneinheit, menge, einzelpreis, gesamtpreis, + waehrung (default EUR), preisstand, wiederkehrend, laufzeitbezug, + subunternehmeranteil, subunternehmer, vergleichsgewicht, gewichtungsbegruendung, kommentar, los +- `vergleichsgewicht` als DecimalField-Input mit Schritt 0.1, min=0.0, max=2.0 +- Widget für `vergleichsgewicht`: `` +- `clean_vergleichsgewicht()`: prüft Decimal('0.0') ≤ wert ≤ Decimal('2.0'), + sonst ValidationError("Vergleichsgewicht muss zwischen 0,0 und 2,0 liegen.") +- `initial={'vergleichsgewicht': Decimal('1.0')}` + +Template `preise/form.html`: +- Abschnitt "Leistung": leistungstyp (mit Datalist für Autovervollständigung bekannter Leistungstypen), + konkrete_leistung, Mengenfelder +- Abschnitt "Preis": einzelpreis, gesamtpreis, waehrung, preisstand +- Abschnitt "Vergleichsgewicht": Numerisches Input + Hilfetextlabel + "0,0 = nicht gewertet | 1,0 = Standard | 2,0 = doppelt gewichtet" +- Subunternehmer-Toggle (Alpine x-show) +``` + +```task +id: WP-0008-T02 +title: Preispunkt-Liste pro Ausschreibung +status: todo + +`preise/views.py` — preispunkte_liste: + +Zeigt alle Preispunkte der Ausschreibung, grupierbar nach Leistungstyp oder Los. +Filter: Leistungstyp, Los, Subunternehmeranteil ja/nein. + +Template `preise/liste.html`: +- Tabelle: Leistungstyp, konkrete Leistung, Menge/Einheit, Einzelpreis, Gesamtpreis, Gewicht, Los +- Gewicht < 1.0: Grauer Text; Gewicht > 1.0: fetter Text; Gewicht = 0.0: durchgestrichen +- Spaltensumme Gesamtpreis (ungewichtet) am Ende der Tabelle +- "+ Preispunkt" Button + +Gesamtpreis-Auto-Berechnung: +```html + + +``` +``` + +```task +id: WP-0008-T03 +title: Leistungstyp-Auswertung mit gewichtetem Durchschnitt (UC-PR-03) +status: todo + +`preise/views.py` — leistungstyp_auswertung: + +```python +def leistungstyp_auswertung(request, ausschreibung_id): + from vergabe_teilnahme.apps.core.services import gewichteter_durchschnitt + ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id) + + leistungstyp = request.GET.get('leistungstyp') + filter_gewonnen = request.GET.get('gewonnen') # 'ja'/'nein'/None + + qs = Preispunkt.objects.filter(einzelpreis__isnull=False) + if leistungstyp: + qs = qs.filter(leistungstyp__icontains=leistungstyp) + if filter_gewonnen == 'ja': + qs = qs.filter(ausschreibung_gewonnen=True) + elif filter_gewonnen == 'nein': + qs = qs.filter(ausschreibung_gewonnen=False) + + ergebnis = gewichteter_durchschnitt(list(qs)) + alle_leistungstypen = Preispunkt.objects.values_list('leistungstyp', flat=True).distinct() + + ctx = { + 'ausschreibung': ausschreibung, + 'leistungstyp': leistungstyp, + 'ergebnis': ergebnis, + 'preispunkte': qs.order_by('-ausschreibung__erstellt_am'), + 'alle_leistungstypen': alle_leistungstypen, + } + return render(request, 'preise/auswertung.html', ctx) +``` + +Template `preise/auswertung.html`: +Zeigt Statistik-Kacheln: Gewichteter Durchschnitt, Ungewichteter Durchschnitt, +Anzahl Messpunkte, Summe Gewichte, Minimum, Maximum. +Darunter: Tabelle aller Einzelmesspunkte mit Ausschreibungstitel, Datum, Gewicht. +``` + +```task +id: WP-0008-T04 +title: Globaler Preisvergleich (cross-Ausschreibung, UC-PR-03) +status: todo + +URL: `/preise/vergleich/` (globaler Endpunkt, kein ausschreibung_id Präfix) + +`preise/views.py` — globaler_preisvergleich: +Gleiche Logik wie leistungstyp_auswertung, aber über alle Ausschreibungen. +Zusätzliche Filter: Zeitraum (von/bis), Ausschreibungstyp (öffentlich/privat via Ausschreiber-Feld), +nur gewonnene / nur verlorene. + +Template `preise/globaler_vergleich.html`: +- Filterleiste oben (HTMX-Update der Ergebnisse) +- Datalist für Leistungstyp-Autocomplete aus allen existierenden Leistungstypen +- Statistik-Kacheln +- Aufschlüsselung: Durchschnitt bei Gewinn vs. Verlust (falls Daten vorhanden) + +URL: `path('preise/vergleich/', preise_views.globaler_preisvergleich, name='preisvergleich')` +in Haupt-URLs einbinden. +``` + +```task +id: WP-0008-T05 +title: Preisfreigabe und URL-Verkabelung (UC-PR-04) +status: todo + +`preise/views.py` — preisfreigabe: +Button "Preisfreigabe erteilen" auf der Preisliste öffnet Freigabe-Modal (aus WP-0012). +Freigabe-Typ: 'preis'. + +Sobald eine Preisfreigabe mit `status='erteilt'` für die Ausschreibung vorliegt, +zeigt die Abgabe-Checkliste (WP-0009) den Preisfreigabe-Punkt als abgehakt. + +`preise/urls.py`: +```python +app_name = 'preise' +urlpatterns = [ + path('', views.preispunkte_liste, name='liste'), + path('neu/', views.preispunkt_neu, name='neu'), + path('/', views.preispunkt_detail, name='detail'), + path('/bearbeiten/', views.preispunkt_bearbeiten, name='bearbeiten'), + path('/loeschen/', views.preispunkt_loeschen, name='loeschen'), + path('auswertung/', views.leistungstyp_auswertung, name='auswertung'), +] +``` + +Tests: +- Test: Vergleichsgewicht 0.0 → gespeichert, nicht in Durchschnitt +- Test: Vergleichsgewicht 2.5 → ValidationError +- Test: gewichteter_durchschnitt mit Blueprint-Beispiel → 103.33 (auf 2 Stellen) +- Test: Auswertungs-View gibt 200 zurück, enthält 'ergebnis' in Context +``` diff --git a/workplans/WP-0009-abgabe-nachbetrachtung.md b/workplans/WP-0009-abgabe-nachbetrachtung.md new file mode 100644 index 0000000..46d671b --- /dev/null +++ b/workplans/WP-0009-abgabe-nachbetrachtung.md @@ -0,0 +1,201 @@ +--- +id: WP-0009 +title: Abgabe und Nachbetrachtung +status: todo +phase: 9-of-12 +created: "2026-05-08" +depends_on: WP-0008 +--- + +# WP-0009 — Abgabe (Phase 6/7) und Nachbetrachtung (Phase 8) + +Abgabe-Checkliste, Vollständigkeitsprüfung, Abgabe-Dokumentation mit Nachweis, +Ergebnis erfassen, Kickoff-Aufgabe erstellen, Verlustanalyse, Lessons Learned. +Referenz: UC-AB-01 bis UC-AB-03, UC-NB-01 bis UC-NB-03. + +--- + +```task +id: WP-0009-T01 +title: Abgabe-Checkliste mit Vollständigkeitsstatus (UC-AB-01) +status: todo + +`nachbetrachtung/abgabe_views.py` — abgabe_checkliste: + +Vollständigkeitsprüfung-Service `abgabe_vollstaendigkeit(ausschreibung)`: +```python +def abgabe_vollstaendigkeit(ausschreibung): + from vergabe_teilnahme.apps.dokumente.models import Dokument + from vergabe_teilnahme.apps.core.models import Freigabe + from django.contrib.contenttypes.models import ContentType + + ct = ContentType.objects.get_for_model(ausschreibung) + freigaben = Freigabe.objects.filter(content_type=ct, object_id=ausschreibung.pk) + + def hat_freigabe(typ): + return freigaben.filter(freigabe_typ=typ, status='erteilt').exists() + + return { + 'dokumente_gesamt': Dokument.objects.filter(ausschreibung=ausschreibung).count(), + 'dokumente_freigegeben': Dokument.objects.filter( + ausschreibung=ausschreibung, status__in=['freigegeben', 'final_abgegeben']).count(), + 'teilnahme_freigabe': hat_freigabe('teilnahme'), + 'preis_freigabe': hat_freigabe('preis'), + 'recht_freigabe': hat_freigabe('recht'), + 'abgabe_freigabe': hat_freigabe('abgabe'), + 'entscheidung_getroffen': ausschreibung.teilnahmeentscheidung == 'teilnahme', + } +``` + +Template `nachbetrachtung/abgabe.html`: +- Fortschrittsbalken oben (Anzahl erfüllter Checkpunkte / Gesamt) +- Checkliste mit Grün-/Rot-Badges für jeden Punkt +- Dokumente-Sektion: Liste aller Dokumente mit Status +- Freigaben-Sektion: Welche Freigaben vorliegen / fehlen +- Frist-Banner: "Abgabe bis: " prominent oben +``` + +```task +id: WP-0009-T02 +title: Abgabe dokumentieren mit Nachweis-Upload (UC-AB-02) +status: todo + +`nachbetrachtung/abgabe_views.py` — abgabe_dokumentieren: + +`AbgabeForm(Form)`: +- `abgabe_zeitpunkt` DateTimeInput(type='datetime-local') +- `abgabe_plattform` CharField +- `verantwortlicher` ModelChoiceField(Mitarbeiter) +- `abgabenachweis` FileField (Eingangsbestätigung, Screenshot etc.) +- `kommentar` Textarea + +```python +def abgabe_dokumentieren(request, ausschreibung_id): + ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id) + if request.method == 'POST': + form = AbgabeForm(request.POST, request.FILES) + if form.is_valid(): + # Abgabenachweis als Dokument speichern + if form.cleaned_data.get('abgabenachweis'): + Dokument.objects.create( + ausschreibung=ausschreibung, + datei=form.cleaned_data['abgabenachweis'], + kategorie='abgabenachweis', + status='final_abgegeben', + finale_abgabeversion=True, + ) + # Ausschreibungsstatus auf Abgegeben setzen + ausschreibung.status = 9 + ausschreibung.save(update_fields=['status', 'geaendert_am']) + # Alle Dokumente mit finale_abgabeversion=True: gesperrt (kein weiterer Upload) + return redirect('ausschreibungen:detail', pk=ausschreibung_id) + else: + form = AbgabeForm() + return render(request, 'nachbetrachtung/abgabe_formular.html', + {'form': form, 'ausschreibung': ausschreibung}) +``` +``` + +```task +id: WP-0009-T03 +title: Nachbetrachtung-View — Ergebnis und Kickoff (UC-NB-01) +status: todo + +`nachbetrachtung/views.py` — nachbetrachtung_detail: + +Erstellt oder lädt die OneToOne `Nachbetrachtung` für die Ausschreibung. + +`NachbetrachtungForm(ModelForm)`: +Felder: ergebnis (Radio), zuschlagsdatum, projektverantwortlicher (Select Mitarbeiter). + +Bei Ergebnis 'gewonnen' (POST): +1. Setze Ausschreibungsstatus auf 10 +2. Erstelle automatisch Aufgabe: +```python +Aufgabe.objects.get_or_create( + ausschreibung=ausschreibung, + titel="Kickoff vorbereiten", + defaults={ + 'typ': 'fachlich', + 'prioritaet': 1, + 'verantwortlicher': form.cleaned_data.get('projektverantwortlicher'), + 'beschreibung': f"Kickoff für {ausschreibung.titel}. Angebotsumfang und Annahmen übergeben.", + } +) +``` +3. Flash-Meldung: "Kickoff-Aufgabe erstellt für " + +Template `nachbetrachtung/detail.html`: +- Ergebnis-Formular oben +- Bei Ergebnis 'gewonnen': Übergabe-Abschnitt (Projektverantwortlicher, Kickoff-Aufgabe-Link) +- Bei Ergebnis 'verloren': Verlustanalyse-Abschnitt (aus UC-NB-02) +``` + +```task +id: WP-0009-T04 +title: Verlustanalyse und Lessons Learned (UC-NB-02, UC-NB-03) +status: todo + +**Verlustgründe** — dynamisches JSONField-Formular: +Alpine.js-gesteuertes Array: +```html +
+ + + +
+``` + +Ausschlaggebende Merkmale, Lessons Learned, Empfehlungen: einfache Textareas. +Checkbox "Wiederverwendbare Erkenntnisse markieren". + +Bei Speichern: Aktualisiere `Nachbetrachtung`-Objekt, Redirect zur Nachbetrachtungsseite. +``` + +```task +id: WP-0009-T05 +title: URL-Verkabelung Abgabe/Nachbetrachtung und Tests +status: todo + +`nachbetrachtung/abgabe_urls.py`: +```python +urlpatterns = [ + path('', abgabe_views.abgabe_checkliste, name='checkliste'), + path('dokumentieren/', abgabe_views.abgabe_dokumentieren, name='dokumentieren'), + path('problem/', abgabe_views.abgabe_problem, name='problem'), +] +``` + +`nachbetrachtung/urls.py`: +```python +app_name = 'nachbetrachtung' +urlpatterns = [ + path('', views.nachbetrachtung_detail, name='detail'), +] +``` + +`abgabe_problem (POST)`: +Setzt `ausschreibung.status` auf internen Marker "Problem bei Abgabe" (eigenes Status-Choice ergänzen wenn nötig, oder Kommentarfeld). + +Tests: +- Test: abgabe_vollstaendigkeit ohne Freigaben → alle False +- Test: Freigabe erteilen → entsprechendes Feld True +- Test: Ergebnis 'gewonnen' → Kickoff-Aufgabe wird erstellt, Status 10 +- Test: Ergebnis 'verloren' → Status 11 +- Test: Verlustgründe JSONField gespeichert mit korrekter Struktur +``` diff --git a/workplans/WP-0010-partner-bibliothek.md b/workplans/WP-0010-partner-bibliothek.md new file mode 100644 index 0000000..d561654 --- /dev/null +++ b/workplans/WP-0010-partner-bibliothek.md @@ -0,0 +1,219 @@ +--- +id: WP-0010 +title: Subunternehmer, Partner und Bibliothek +status: todo +phase: 10-of-12 +created: "2026-05-08" +depends_on: WP-0009 +--- + +# WP-0010 — Subunternehmer, Partner und Bibliothek + +Subunternehmer-Katalog, Dienstleistertypen, Nachweis-/Referenz-/Leistungsblatt-/ +Entscheidungsregel-Verwaltung. Referenz: UC-SU-01 bis UC-SU-04, UC-BIB-01 bis UC-BIB-05. + +--- + +```task +id: WP-0010-T01 +title: Subunternehmer-Katalog: Liste, Suche, Anlegen (UC-SU-01, UC-SU-03) +status: todo + +`partner/views.py` — subunternehmer_liste, subunternehmer_neu: + +Liste: Filter nach Dienstleistertyp, Präferenz (bevorzugt/zugelassen/gesperrt), Freitext-Suche. +`Subunternehmer.objects.filter(name__icontains=q)` für Freitext. +Präferenz 'gesperrt': Rot-Badge, wird in Suchergebnissen mit Warnsymbol angezeigt. + +`SubunternehmerForm(ModelForm)`: alle Felder, Präferenz als Radio-Buttons. + +subunternehmer_detail: Stammdaten + verknüpfte Ausschreibungen (über SubunternehmerZuordnung): +```python +zuordnungen = SubunternehmerZuordnung.objects.filter( + subunternehmer=obj +).select_related('ausschreibung', 'los').order_by('-ausschreibung__erstellt_am') +``` +Zeigt: Ausschreibung, Los, Leistung, Zusage/Nachweis/Preis-Status. +CustomAttribute-Panel. +``` + +```task +id: WP-0010-T02 +title: Subunternehmer einer Ausschreibung/Los zuordnen (UC-SU-02) +status: todo + +`partner/views.py` — subunternehmer_zuordnen: + +HTMX-Modal auf Los-Detail-Seite: +```python +def subunternehmer_suche_modal(request, ausschreibung_id, los_pk): + q = request.GET.get('q', '') + subunternehmer = Subunternehmer.objects.filter(name__icontains=q) + return render(request, 'partner/partials/subunternehmer_suche.html', + {'subunternehmer': subunternehmer, 'los_pk': los_pk, + 'ausschreibung_id': ausschreibung_id}) + +def subunternehmer_zuordnen(request, ausschreibung_id, los_pk): + if request.method == 'POST': + sub_id = request.POST['subunternehmer_id'] + sub = get_object_or_404(Subunternehmer, pk=sub_id) + if sub.praeferenz == 'gesperrt': + # Warnung anzeigen aber nicht blockieren + pass + los = get_object_or_404(Los, pk=los_pk) + zuordnung, created = 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}) +``` + +Auf der Los-Detail-Seite zeigt jede Zuordnung: Name, Dienstleistertyp, Leistung, +drei Checkboxen (Zusage, Nachweis, Preis) — HTMX-togglebar. +``` + +```task +id: WP-0010-T03 +title: Dienstleistertyp-Katalog und Subunternehmer als gesperrt markieren (UC-SU-04) +status: todo + +`partner/views.py` — dienstleistertypen_liste, dienstleistertyp_neu/_bearbeiten: +Einfache CRUD-Views für Dienstleistertypen (Katalog-Daten). + +`subunternehmer_praeferenz (POST)`: +```python +def subunternehmer_praeferenz(request, pk): + sub = get_object_or_404(Subunternehmer, pk=pk) + if request.method == 'POST': + sub.praeferenz = request.POST['praeferenz'] + if sub.praeferenz == 'gesperrt': + sub.bewertung = request.POST.get('begruendung', sub.bewertung) + sub.save(update_fields=['praeferenz', 'bewertung']) + return render(request, 'partner/partials/praeferenz_badge.html', {'sub': sub}) +``` + +Bei Präferenz 'gesperrt': Roter Warnhinweis wenn dieser Subunternehmer bei +Zuordnung gewählt wird (im Suchmodal). + +`partner/urls.py`: +```python +app_name = 'partner' +urlpatterns = [ + path('subunternehmer/', views.subunternehmer_liste, name='su_liste'), + path('subunternehmer/neu/', views.subunternehmer_neu, name='su_neu'), + path('subunternehmer//', views.subunternehmer_detail, name='su_detail'), + path('subunternehmer//bearbeiten/', views.subunternehmer_bearbeiten, name='su_bearbeiten'), + path('subunternehmer//praeferenz/', views.subunternehmer_praeferenz, name='su_praeferenz'), + path('dienstleistertypen/', views.dienstleistertypen_liste, name='dt_liste'), + path('dienstleistertypen/neu/', views.dienstleistertyp_neu, name='dt_neu'), +] +``` +``` + +```task +id: WP-0010-T04 +title: Bibliothek: Nachweis-Katalog mit Ablaufwarnung (UC-BIB-01, UC-BIB-02) +status: todo + +`bibliothek/views.py` — nachweise_liste, nachweis_neu/_bearbeiten: + +Liste mit Ablauffilter: +```python +from datetime import date, timedelta +heute = date.today() +in_60_tagen = heute + timedelta(days=60) +``` +Tabs: "Alle", "Bald ablaufend" (`gueltig_bis__lte=in_60_tagen`), "Abgelaufen" (`gueltig_bis__lt=heute`). +Abgelaufene Nachweise: Roter Badge. Bald ablaufende: Oranger Badge. + +`NachweisForm(ModelForm)`: +`datei` als FileInput. `gueltig_ab`/`gueltig_bis` als DateInput type="date". +Checkboxen für fuer_oeffentliche/fuer_privatwirtschaftliche. + +nachweis_neue_version: Analog zu Dokument-Versionierung (WP-0007-T03). +Alten Nachweis auf status='ersetzt', neuen anlegen mit höherer Version. + +`bibliothek/urls.py` (Auszug): +```python +path('nachweise/', views.nachweise_liste, name='nachweise_liste'), +path('nachweise/neu/', views.nachweis_neu, name='nachweis_neu'), +path('nachweise//', views.nachweis_detail, name='nachweis_detail'), +path('nachweise//version/', views.nachweis_neue_version, name='nachweis_version'), +``` +``` + +```task +id: WP-0010-T05 +title: Bibliothek: Referenz anlegen und zuordnen (UC-BIB-03, UC-BIB-04) +status: todo + +`bibliothek/views.py` — referenzen_liste, referenz_neu/_bearbeiten: + +`ReferenzForm(ModelForm)`: +`whitepaper` als FileInput. `projektzeitraum` als CharField (freies Datum-Format: "2024-2025"). +`leistungsblaetter` als CheckboxSelectMultiple. + +referenz_detail: Zeigt alle Felder, Whitepaper-Download-Link, verknüpfte Leistungsblätter. + +`referenz_zuordnen (POST)`: +Wird von Ausschreibungs-Abgabe-Seite aufgerufen. +Erstellt eine M2M-Verknüpfung zwischen Referenz und Ausschreibung. +(Ergänze `referenzen` M2M-Feld auf Ausschreibung-Modell + Migration) + +HTMX-Suchmodal: Suche nach Branche, Leistungsbeschreibung, Titel. +Zeigt Freigabestatus und Nutzungseinschränkungen als Warnung. +``` + +```task +id: WP-0010-T06 +title: Bibliothek: Leistungsblatt und Entscheidungsregel (UC-BIB-05) +status: todo + +`bibliothek/views.py` — leistungsblaetter_liste, leistungsblatt_neu/_bearbeiten: +Einfache CRUD-Views. `LeistungsblattForm(ModelForm)` mit allen Textfeldern. + +`bibliothek/views.py` — entscheidungsregeln_liste, entscheidungsregel_neu/_bearbeiten: + +`EntscheidungsregelForm(ModelForm)`: +`kategorie` als Select mit Choices: +[(ausschlusskriterium, 'Ausschlusskriterium'), (frist, 'Fristlage'), + (referenz, 'Referenzanforderung'), (wirtschaftlichkeit, 'Wirtschaftlichkeit'), + (ressourcen, 'Ressourcenverfügbarkeit'), (sonstiges, 'Sonstiges')] + +`empfehlung` als Radio-Buttons (teilnehmen/nicht_teilnehmen/pruefen). +`aktiv`-Toggle in der Liste (HTMX POST). + +Auf der Entscheidungsseite (Phase 2) werden nur `aktiv=True` Regeln angezeigt. +``` + +```task +id: WP-0010-T07 +title: Bibliothek URL-Verkabelung und Tests +status: todo + +`bibliothek/urls.py` vollständig: +```python +app_name = 'bibliothek' +urlpatterns = [ + path('nachweise/', ...), + path('referenzen/', ...), + path('leistungsblaetter/', ...), + path('entscheidungsregeln/', ...), + # Detail/Neu/Bearbeiten für jede Entität +] +``` + +Global in Haupt-URLs: +`path('bibliothek/', include('vergabe_teilnahme.apps.bibliothek.urls'))` +`path('partner/', include('vergabe_teilnahme.apps.partner.urls'))` + +Tests: +- Test: Nachweis mit abgelaufenem gueltig_bis → `ist_abgelaufen` property True +- Test: Nachweis-Liste mit Filter "Abgelaufen" → nur abgelaufene sichtbar +- Test: Subunternehmer-Zuordnung zu Los → SubunternehmerZuordnung-Objekt in DB +- Test: Gesperrter Subunternehmer → Warnung im Modal sichtbar (Template enthält 'gesperrt') +- Test: Entscheidungsregel mit aktiv=False → erscheint nicht in Phase-2-Auswertung +``` diff --git a/workplans/WP-0011-marktbegleiter.md b/workplans/WP-0011-marktbegleiter.md new file mode 100644 index 0000000..2d288a4 --- /dev/null +++ b/workplans/WP-0011-marktbegleiter.md @@ -0,0 +1,131 @@ +--- +id: WP-0011 +title: Marktbegleiter-Analyse +status: todo +phase: 11-of-12 +created: "2026-05-08" +depends_on: WP-0010 +--- + +# WP-0011 — Marktbegleiter-Analyse + +Marktbegleiter-Katalog, Passagen-Erfassung mit Verlässlichkeitsscore, +Musterauswertung. Referenz: UC-MB-01 bis UC-MB-03. + +--- + +```task +id: WP-0011-T01 +title: Marktbegleiter-Katalog: Liste und Anlegen (UC-MB-01) +status: todo + +`marktbegleiter/views.py` — marktbegleiter_liste, marktbegleiter_neu/_bearbeiten: + +Liste: Tabellenansicht mit Name, Branchen (gekürzt), Aktualisierungsdatum. +Filter: Branche (Freitext-Match auf `relevante_branchen`). + +`MarktbegleiterForm(ModelForm)`: +`typische_formulierungen` als Textarea mit Placeholder "Eine Formulierung pro Zeile". +`vertraulichkeit` als Select (intern/streng_vertraulich). + +marktbegleiter_detail: +- Profil-Abschnitte: Portfolio, Stärken/Schwächen, typische Formulierungen (als Tags/Pilllen) +- Abschnitt "Verknüpfte Passagen": Liste aller Ausschreibungspassagen, sortiert nach Verlässlichkeit desc +- Aggregation: "Erscheint in X Ausschreibungen, Ø Verlässlichkeit: Y" +- CustomAttribute-Panel +``` + +```task +id: WP-0011-T02 +title: Ausschreibungspassage erfassen (UC-MB-02, UC-MB-03) +status: todo + +`marktbegleiter/passagen_views.py` — passagen_liste und passage_neu: + +passagen_liste (pro Ausschreibung): +`Ausschreibungspassage.objects.filter(ausschreibung_id=ausschreibung_id).select_related('marktbegleiter', 'dokument')` +Template `marktbegleiter/passagen_liste.html`: Tabelle mit Textauszug (150 Zeichen), Marktbegleiter, +Score, Kategorie, Datum. + +`AusschreibungspassageForm(ModelForm)`: +- `passage` als großes Textarea +- `verlaesslichkeitsscore` als Range-Input 1-10 (mit Alpine.js-Beschriftung: "1=sehr unsicher, 10=sehr sicher") +- `marktbegleiter` als Select + Link "Neuen Marktbegleiter anlegen" (öffnet Modal) +- `dokument` als Select (nur Dokumente dieser Ausschreibung) +- `kategorie` als Select: formulierung/leistungsmerkmal/zertifizierung/referenz/sonstiges + +Nach Speichern: Passage erscheint auf Ausschreibungsdetail (Tab "Marktbegleiter") +und im Marktbegleiter-Profil. +``` + +```task +id: WP-0011-T03 +title: Marktbegleiter-Musterauswertung (UC-MB-03) +status: todo + +`marktbegleiter/views.py` — marktbegleiter_auswertung: + +```python +def marktbegleiter_auswertung(request, pk): + mb = get_object_or_404(Marktbegleiter, pk=pk) + passagen = Ausschreibungspassage.objects.filter(marktbegleiter=mb).select_related( + 'ausschreibung', 'dokument' + ) + # Aggregationen + ausschreiber_haeufigkeit = passagen.values( + 'ausschreibung__ausschreiber' + ).annotate(count=Count('id')).order_by('-count')[:10] + + score_durchschnitt = passagen.aggregate(Avg('verlaesslichkeitsscore')) + + ctx = { + 'marktbegleiter': mb, + 'passagen': passagen, + 'ausschreiber_haeufigkeit': ausschreiber_haeufigkeit, + 'score_durchschnitt': score_durchschnitt['verlaesslichkeitsscore__avg'], + 'anzahl_ausschreibungen': passagen.values('ausschreibung').distinct().count(), + } + return render(request, 'marktbegleiter/auswertung.html', ctx) +``` + +Template `marktbegleiter/auswertung.html`: +- Statistikkacheln: Anzahl Passagen, Anzahl Ausschreibungen, Ø Score +- Tabelle: Häufigste Ausschreiber +- Alle Passagen sortierbar nach Datum / Score / Ausschreibung +``` + +```task +id: WP-0011-T04 +title: URL-Verkabelung und Tests +status: todo + +`marktbegleiter/passagen_urls.py`: +```python +urlpatterns = [ + path('', passagen_views.passagen_liste, name='liste'), + path('neu/', passagen_views.passage_neu, name='neu'), + path('/', passagen_views.passage_detail, name='detail'), + path('/bearbeiten/', passagen_views.passage_bearbeiten, name='bearbeiten'), +] +``` + +`marktbegleiter/urls.py`: +```python +app_name = 'marktbegleiter' +urlpatterns = [ + path('', views.marktbegleiter_liste, name='liste'), + path('neu/', views.marktbegleiter_neu, name='neu'), + path('/', views.marktbegleiter_detail, name='detail'), + path('/auswertung/', views.marktbegleiter_auswertung, name='auswertung'), + path('/bearbeiten/', views.marktbegleiter_bearbeiten, name='bearbeiten'), +] +``` + +Global in Haupt-URLs: `path('marktbegleiter/', include('vergabe_teilnahme.apps.marktbegleiter.urls'))` + +Tests: +- Test: Passage anlegen mit verlaesslichkeitsscore=10 → gespeichert +- Test: verlaesslichkeitsscore=11 → ValidationError (MinValueValidator/MaxValueValidator) +- Test: Auswertungs-View mit Passagen → score_durchschnitt korrekt berechnet +- Test: Marktbegleiter-Detail zeigt Passagen-Liste +``` diff --git a/workplans/WP-0012-querschnitt.md b/workplans/WP-0012-querschnitt.md new file mode 100644 index 0000000..eeb0f63 --- /dev/null +++ b/workplans/WP-0012-querschnitt.md @@ -0,0 +1,346 @@ +--- +id: WP-0012 +title: Querschnitt — Freigaben, Flexible Felder, Feedback, Suche, Tests +status: todo +phase: 12-of-12 +created: "2026-05-08" +depends_on: WP-0011 +--- + +# WP-0012 — Querschnitt + +Generisches Freigabe-Modal, EntityFieldConfig Admin-UI, CustomAttribute-Panel, +Feedback vollständig, globale Suche fertigstellen, End-to-End-Tests. +Referenz: UC-FR-01, UC-FR-02, UC-FF-01 bis UC-FF-03, UC-FB-01, UC-FB-02. + +--- + +```task +id: WP-0012-T01 +title: Generisches Freigabe-Modal (UC-FR-01) +status: todo + +`core/views.py` — freigabe_modal und freigabe_erteilen: + +```python +def freigabe_modal(request): + """Gibt das Freigabe-Formular-Modal als Fragment zurück.""" + content_type_id = request.GET.get('ct') + object_id = request.GET.get('oid') + freigabe_typ = request.GET.get('typ') + ctx = { + 'content_type_id': content_type_id, + 'object_id': object_id, + 'freigabe_typ': freigabe_typ, + 'freigabe_typ_choices': Freigabe.TYP_CHOICES, + } + return render(request, 'partials/freigabe_modal.html', ctx) + +def freigabe_erteilen(request): + """Speichert eine Freigabe und gibt Success-Fragment zurück.""" + if request.method == 'POST': + ct = ContentType.objects.get(pk=request.POST['content_type_id']) + Freigabe.objects.create( + content_type=ct, + object_id=request.POST['object_id'], + freigabe_typ=request.POST['freigabe_typ'], + freigebende_person=request.user, + status='erteilt', + kommentar=request.POST.get('kommentar', ''), + ) + return render(request, 'partials/freigabe_success.html', {}) + return HttpResponseBadRequest() +``` + +`partials/freigabe_modal.html`: +Modal mit Typ-Dropdown (vorausgefüllt wenn übergeben), Kommentarfeld, "Freigabe erteilen"-Button. +`hx-post="/freigaben/erteilen/" hx-target="#modal-container"` + +Nutzung in anderen Templates: +```html + +``` + +Hilfsfunktion `get_content_type_id(model_instance)` in core/templatetags. + +URL: `path('freigaben/modal/', core_views.freigabe_modal, name='freigabe_modal')` + `path('freigaben/erteilen/', core_views.freigabe_erteilen, name='freigabe_erteilen')` +``` + +```task +id: WP-0012-T02 +title: Freigaben-Übersicht pro Ausschreibung (UC-FR-02) +status: todo + +`ausschreibungen/views.py` — freigaben_uebersicht: +```python +def freigaben_uebersicht(request, pk): + ausschreibung = get_object_or_404(Ausschreibung, pk=pk) + ct = ContentType.objects.get_for_model(ausschreibung) + freigaben = Freigabe.objects.filter( + content_type=ct, object_id=pk + ).select_related('freigebende_person').order_by('-timestamp') + + # Welche Freigabetypen fehlen noch? + erteilte_typen = set(freigaben.filter(status='erteilt').values_list('freigabe_typ', flat=True)) + erforderliche_typen = {'teilnahme', 'preis', 'abgabe'} + fehlende_typen = erforderliche_typen - erteilte_typen + + ctx = { + 'ausschreibung': ausschreibung, + 'freigaben': freigaben, + 'fehlende_typen': fehlende_typen, + 'breadcrumbs': [...], + } + return render(request, 'ausschreibungen/freigaben.html', ctx) +``` + +Template: Tabelle mit allen Freigaben + roter Banner für fehlende Pflichtfreigaben. +Auf Detailseite: Tab "Freigaben" → diese View. + +URL: `path('/freigaben/', views.freigaben_uebersicht, name='freigaben')` +``` + +```task +id: WP-0012-T03 +title: EntityFieldConfig Admin-Interface (UC-FF-01, UC-FF-02) +status: todo + +`core/views.py` — feld_konfiguration_liste und feld_konfiguration_toggle: + +Nicht der Django-Standard-Admin, sondern eine eigene Verwaltungsseite unter `/admin/felder/`. + +```python +ENTITY_TYPES = [ + ('ausschreibung', Ausschreibung), + ('los', Los), + ('anforderung', Anforderung), + ('aufgabe', Aufgabe), + ('subunternehmer', Subunternehmer), + ('nachweis', Nachweis), + ('referenz', Referenz), + # alle FlexibleModel-Unterklassen +] + +def feld_konfiguration_liste(request, entity_type): + model = dict(ENTITY_TYPES)[entity_type] + felder = [f for f in model._meta.get_fields() + if hasattr(f, 'column') and not f.name.startswith('_')] + konfigurationen = { + cfg.field_name: cfg + for cfg in EntityFieldConfig.objects.filter(entity_type=entity_type) + } + return render(request, 'core/feld_konfiguration.html', { + 'entity_type': entity_type, 'felder': felder, 'konfigurationen': konfigurationen + }) + +def feld_konfiguration_toggle(request, entity_type, field_name): + cfg, _ = EntityFieldConfig.objects.get_or_create( + entity_type=entity_type, field_name=field_name) + if request.method == 'POST': + cfg.is_hidden = request.POST.get('is_hidden') == 'true' + cfg.display_label = request.POST.get('display_label', '') + cfg.save() + return render(request, 'core/partials/feld_zeile.html', {'cfg': cfg, 'field_name': field_name}) +``` + +Template `core/feld_konfiguration.html`: +Pro Feld eine Zeile mit: Feldname, Anzeige-Label-Input, Ausblenden-Toggle (HTMX). +Änderungen sofort aktiv. +``` + +```task +id: WP-0012-T04 +title: CustomAttribute-Panel für alle Detailseiten (UC-FF-03) +status: todo + +`core/views.py` — custom_attributes_panel, custom_attribute_neu, custom_attribute_bearbeiten: + +```python +def custom_attributes_panel(request, content_type_id, object_id): + ct = get_object_or_404(ContentType, pk=content_type_id) + attrs = CustomAttribute.objects.filter(content_type=ct, object_id=object_id) + return render(request, 'core/partials/custom_attributes.html', + {'attrs': attrs, 'ct_id': content_type_id, 'oid': object_id}) + +def custom_attribute_neu(request, content_type_id, object_id): + ct = get_object_or_404(ContentType, pk=content_type_id) + if request.method == 'POST': + CustomAttribute.objects.create( + content_type=ct, + object_id=object_id, + key=slugify(request.POST['label']), + label=request.POST['label'], + value=request.POST.get('value', ''), + data_type=request.POST.get('data_type', 'text'), + ) + return redirect_to_panel(content_type_id, object_id) +``` + +`core/partials/custom_attributes.html`: +HTMX lazy-load aus Detailseiten: +```html +
Lade...
+``` + +Formular für neues Attribut: +Alpine.js `x-show="form_open"` Toggle. +Felder: Label, Wert, Datentyp (Select: text/number/date/boolean/url/email). +Bestehende Attribute: inline bearbeitbar, löschbar via HTMX DELETE. +Sortierung via Up/Down-Buttons (HTMX POST auf `custom_attribute_sort`). + +URLs: +```python +path('core/attrs///', custom_attributes_panel), +path('core/attrs///neu/', custom_attribute_neu), +path('core/attrs////bearbeiten/', custom_attribute_bearbeiten), +path('core/attrs////loeschen/', custom_attribute_loeschen), +path('core/attrs////sort/', custom_attribute_sort), +``` +``` + +```task +id: WP-0012-T05 +title: Feedback vollständig: Modal-POST und Backlog-View (UC-FB-01, UC-FB-02) +status: todo + +`feedback/views.py` — feedback_modal, feedback_speichern, feedback_backlog: + +feedback_speichern (POST): +```python +def feedback_speichern(request): + if request.method == 'POST': + Feedbackeintrag.objects.create( + titel=request.POST.get('titel', 'Ohne Titel'), + beschreibung=request.POST['beschreibung'], + kategorie=request.POST.get('kategorie', 'hinweis'), + dringlichkeit=request.POST.get('dringlichkeit', 'mittel'), + seite_kontext=request.POST.get('seite_kontext', ''), + ausschreibung_id=request.POST.get('ausschreibung') or None, + erfasst_von=request.user if request.user.is_authenticated else None, + ) + return render(request, 'partials/feedback_success.html') +``` + +`partials/feedback_success.html`: Danke-Meldung die nach 3 Sekunden verschwindet +(Alpine.js `x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show=false, 3000)"`). + +feedback_backlog: Liste aller Einträge mit Filter nach Status, Kategorie, Dringlichkeit. +Admin kann Status ändern (neu/in_bearbeitung/umgesetzt/abgelehnt), Bewertung und Entscheidung eintragen. +``` + +```task +id: WP-0012-T06 +title: End-to-End-Tests für kritische Use Cases +status: todo + +Erstelle `vergabe_teilnahme/tests/test_e2e.py` mit vollständigen Prozess-Tests: + +```python +import pytest +from django.test import Client + +@pytest.mark.django_db +class TestVollstaendigerBieterprozess: + def test_ausschreibung_anlegen_bis_abgabe(self, client, mitarbeiter): + client.force_login(mitarbeiter) + # 1. Ausschreibung anlegen + r = client.post('/ausschreibungen/neu/', {'titel': 'Test', 'ausschreiber': 'ABC GmbH', ...}) + assert r.status_code == 302 + ausschreibung_id = Ausschreibung.objects.last().pk + + # 2. Los anlegen + r = client.post(f'/ausschreibungen/{ausschreibung_id}/lose/neu/', {...}) + assert Los.objects.filter(ausschreibung_id=ausschreibung_id).count() == 1 + + # 3. Anforderung anlegen + # 4. Aufgabe anlegen + # 5. Preispunkt mit Vergleichsgewicht 1.5 anlegen + p = Preispunkt.objects.last() + assert p.vergleichsgewicht == Decimal('1.5') + + # 6. Freigabe erteilen + r = client.post('/freigaben/erteilen/', { + 'content_type_id': ct_id, 'object_id': ausschreibung_id, + 'freigabe_typ': 'teilnahme' + }) + assert Freigabe.objects.filter(freigabe_typ='teilnahme').exists() + + # 7. Abgabe dokumentieren + r = client.post(f'/ausschreibungen/{ausschreibung_id}/abgabe/dokumentieren/', {...}) + ausschreibung.refresh_from_db() + assert ausschreibung.status == 9 + +@pytest.mark.django_db +class TestFlexibleFelder: + def test_feld_ausblenden_wirkt_im_template(self, client, mitarbeiter): + sub = Subunternehmer.objects.create(name='Test GmbH', mobilnummer='0123') + EntityFieldConfig.objects.create( + entity_type='subunternehmer', field_name='mobilnummer', is_hidden=True) + client.force_login(mitarbeiter) + r = client.get(f'/partner/subunternehmer/{sub.pk}/') + assert 'mobilnummer' not in r.content.decode().lower() or 'Mobilnummer' not in r.content.decode() + + def test_custom_attribute_hinzufuegen(self, client, mitarbeiter): + sub = Subunternehmer.objects.create(name='Test GmbH') + ct = ContentType.objects.get_for_model(sub) + client.force_login(mitarbeiter) + r = client.post(f'/core/attrs/{ct.pk}/{sub.pk}/neu/', + {'label': 'Vertragsnummer', 'value': 'VN-2026-001', 'data_type': 'text'}) + assert CustomAttribute.objects.filter(object_id=sub.pk, label='Vertragsnummer').exists() +``` +``` + +```task +id: WP-0012-T07 +title: Globale Suche vervollständigen und Performance-Prüfung +status: todo + +Vervollständige `core/views.py` — global_search: +- Dokumente (dateiname__icontains) +- Nachweise (titel__icontains) +- Referenzen (referenztitel__icontains, kunde__icontains) +- Marktbegleiter (name__icontains) + +Optimierung: Alle Queries in einem einzigen DB-Roundtrip via `select_related` und +Begrenzung auf 5 Treffer pro Kategorie. + +`partials/search_results.html` vollständig: +Alle sechs Ergebniskategorien mit Icon und Link. +Leer-State: "Keine Ergebnisse für ''" wenn alle leer. + +Performance-Test: Prüfe mit `uv run manage.py shell -c "..."` und `django.test.utils.CaptureQueriesContext` +dass die Such-View ≤ 6 DB-Queries ausführt (eine pro Entitätstyp + ContentType). +``` + +```task +id: WP-0012-T08 +title: Finaler Integrations-Smoke-Test und CLAUDE.md-Aktualisierung +status: todo + +Führe den finalen Integrations-Test durch: + +1. Alle Migrationen sauber: `uv run manage.py migrate` → 0 Fehler +2. Seed-Daten: `uv run manage.py seed_dev` → 0 Fehler +3. `uv run pytest` → alle Tests grün, Testabdeckung ≥ 60% für kritische Module +4. `uv run ruff check .` → 0 Fehler +5. Manueller Smoke-Test aller Hauptseiten: + - Dashboard, Ausschreibungsliste, Ausschreibung-Detail + - Lose-Liste, Anforderungs-Liste, Aufgaben-Liste + - Bieterfragen-Liste, Dokumente-Liste, Preisliste + - Abgabe-Checkliste, Nachbetrachtung + - Subunternehmer-Katalog, Bibliothek (Nachweise, Referenzen) + - Marktbegleiter-Liste + - Feedback-Modal (auf jeder getesteten Seite) + - Admin-Felder-Konfiguration + - Globale Suche mit Suchbegriff + +6. Aktualisiere `CLAUDE.md`: + - Bestätige alle Build-Commands als korrekt + - Ergänze "Testabdeckung" und "Produktionsdeployment"-Hinweis + - Notiere bekannte v1-Limitierungen (z. B. kein Celery für Fristenbenachrichtigungen) + +Erst wenn alle 6 Punkte erfüllt: Workplan als done markieren. +```