--- id: WP-0003 title: Basis-UI — Shell-Layout, Templates, Template-Tags, Navigation status: done 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: done 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: done `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: done `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: done **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: done `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: done 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: done 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: done `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: done `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: done 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. ```