---
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
```
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
```
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.
```