# Schritt 1: Datenmodell erstellen
[← Zurück zur Übersicht](README.md) | [Weiter: Interface erstellen →](03-interfaces.md)
## Real Estate-Datenmodelle
**Datei:** `modules/datamodels/datamodelRealEstate.py`
Das Feature arbeitet **stateless** ohne Session-Management. Die Datenmodelle definieren die Struktur der Real Estate-Entitäten, die über die API verwaltet werden können.
**Hinweis:** Die Real Estate-Datenmodell-Entitäten (Projekt, Parzelle, Dokument, etc.) werden in `datamodelRealEstate.py` definiert. Diese werden direkt über CRUD-Operationen verwaltet, ohne zusätzliche Chat-Interface-Modelle.
### Warum keine Chat-Interface-Modelle?
Das Feature arbeitet **stateless** ohne Session-Management. Alle Operationen arbeiten direkt auf den Real Estate-Datenmodellen:
#### Stateless Design
- **Keine Session-Modelle**: Keine `RealEstateChatSession` notwendig
- **Keine Query-History**: Queries werden nicht gespeichert (kann optional später hinzugefügt werden)
- **Direkte CRUD-Operationen**: User-Input → AI-Analyse → CRUD → Ergebnis
- **Einfache Architektur**: Weniger Komplexität, bessere Performance
#### Real Estate-Modelle
Die Real Estate-Modelle (`Projekt`, `Parzelle`, `Dokument`, etc.):
- Repräsentieren die **tatsächlichen Geschäftsdaten** der Immobilien-Projekte
- Werden über lange Zeiträume gepflegt und verändert
- Haben komplexe Beziehungen zueinander (Projekt → Parzellen → Dokumente)
- Werden direkt über CRUD-Operationen verwaltet
#### Datenfluss (stateless)
```
User Input (natürliche Sprache)
↓
AI-Analyse (Intent-Erkennung)
↓
CRUD-Operation identifizieren
↓
Real Estate-Modelle
↓
Datenbank-Operation
↓
Ergebnis zurückgeben
(keine Session, keine History)
```
---
### Stateless vs. Session-basiert
**Real Estate Feature (stateless):**
- Direkte CRUD-Operationen auf Real Estate-Modellen
- Keine Session-Modelle notwendig
- Keine Query-History
- Einfacher und schneller
**Chat-System (session-basiert):**
- Verwendet `ChatWorkflow` für komplexe AI-Workflows
- Verwendet `ChatDocument` für Datei-Verknüpfungen
- Session-Management für Multi-Step-Operationen
- Für komplexe Workflows mit Planung und Review
**Unterschied:**
- Real Estate Feature ist für **einfache CRUD-Operationen** optimiert
- Chat-System ist für **komplexe AI-Workflows** optimiert
- Beide können parallel existieren und für verschiedene Use Cases genutzt werden
---
### Warum nicht das bestehende `ChatWorkflow` verwenden?
Sie fragen sich vielleicht: **Kann ich nicht einfach das bestehende `ChatWorkflow` aus `datamodelChat.py` verwenden?**
Die kurze Antwort: **Für stateless CRUD-Operationen ist `ChatWorkflow` zu komplex**. Das Real Estate Feature arbeitet ohne Session-Management und nutzt direkt die Real Estate-Modelle.
#### Unterschiedliche Anwendungsfälle
| **Aspekt** | **ChatWorkflow (bestehend)** | **Real Estate Feature (stateless)** |
|------------|------------------------------|-------------------------------------|
| **Zweck** | Komplexe AI-gesteuerte Workflows mit mehreren Tasks/Actions | Einfache CRUD-Operationen |
| **Komplexität** | Hoch: Tasks, Actions, Rounds, Workflow-Modi, Retries | Niedrig: Direkte CRUD-Operationen |
| **Session** | Session-Management für Multi-Step-Workflows | Keine Session, stateless |
| **Verarbeitung** | Multi-Step AI-Workflows mit Planung, Review, Iteration | Direkte CRUD: User-Input → AI-Analyse → CRUD → Ergebnis |
| **Ergebnisse** | `ChatMessage` mit `documents`, `ActionResult` | Direkte CRUD-Ergebnisse (Projekt, Parzelle, etc.) |
#### Warum `ChatWorkflow` nicht passt:
1. **Zu komplex**: `ChatWorkflow` hat viele Felder, die für einfache CRUD-Operationen nicht relevant sind
2. **Session-basiert**: `ChatWorkflow` benötigt Session-Management, das wir nicht brauchen
3. **Falsches Abstraktionsniveau**: `ChatWorkflow` ist für komplexe AI-Workflows, Real Estate braucht einfache CRUD-Operationen
#### Die richtige Lösung: Direkte CRUD-Operationen
Stattdessen arbeiten wir **direkt** mit den Real Estate-Modellen:
```python
# Stateless CRUD-Operationen
User Input → AI-Analyse → CRUD-Operation → Ergebnis
# Keine Session, keine History, einfach und schnell
```
#### Wann könnte man `ChatWorkflow` verwenden?
Sie könnten `ChatWorkflow` verwenden, wenn Sie:
- ✅ **Komplexe AI-Workflows** für Real Estate implementieren wollen (z.B. "Analysiere alle Projekte und erstelle einen Bericht")
- ✅ **Multi-Step-Verarbeitung** benötigen (z.B. "Lade Daten → Transformiere → Erstelle Visualisierung")
- ✅ **Planung und Review** brauchen (z.B. "Prüfe alle Parzellen auf Konformität")
Aber für **einfache CRUD-Operationen** ist der stateless Ansatz die bessere Wahl.
---
## Real Estate-Datenmodell-Implementierung:
Die Real Estate-Datenmodell-Entitäten müssen separat in `modules/datamodels/datamodelRealEstate.py` implementiert werden.
Siehe `../PEK_datamodel_desc.md` für die vollständige Spezifikation aller Felder und Beziehungen (PEK ist ein Beispiel für eine Real Estate-Firma, das Modell ist aber allgemein verwendbar).
### Wichtige Hinweise zum Datenmodell
**Objektmodell vs. Datenbank-Repräsentation:**
Dieses Dokument beschreibt ein **Objektmodell** für die Arbeit im Code. Es handelt sich **NICHT** um ein relationales Datenbankmodell mit Junction Tables.
- **Im Code-Modell**: Alle Beziehungen werden als Objektreferenzen oder Listen von Objekten dargestellt (z.B. `dokumente: list[Dokument]`, `parzellen: list[Parzelle]`).
- **Für die Datenbank-Serialisierung**: Bei der Persistierung können Junction Tables verwendet werden, um n:m-Beziehungen in der Datenbank abzubilden. Dies ist jedoch ein Implementierungsdetail der Datenbank-Schicht und gehört nicht zum Hauptmodell.
**Systemattribute:**
Alle Datenobjekte haben automatisch die folgenden Systemattribute:
- `_createdAt`: Float (Timestamp UTC)
- `_createdBy`: String (User-ID)
- `_modifiedAt`: Float (Timestamp UTC)
- `_modifiedBy`: String (User-ID)
**Timestamps:**
- Alle Timestamps sind im Float-Format UTC im Datenmodell gespeichert.
- Die Darstellung im UI erfolgt mit der lokalen Zeitzone des Benutzers.
**Wichtige Punkte für die Implementierung:**
- Objektbeziehungen wie `parzellen: list[Parzelle]` werden als JSONB in PostgreSQL gespeichert
- Einzelne Objektreferenzen wie `kontextKanton: Optional[str]` werden als String-ID (Foreign Key) gespeichert
- Administrative Hierarchie: `Kanton` benötigt `id_land` (Foreign Key zu Land), `Gemeinde` benötigt `id_kanton` (Foreign Key zu Kanton)
- Alle Entitäten benötigen `mandateId` für Mandaten-Isolation
- Systemattribute (`_createdAt`, `_createdBy`, etc.) werden automatisch vom DatabaseConnector hinzugefügt
### Datenfluss-Diagramm
```mermaid
---
title: Hauptflüsse - Architektur-Planungs-App
---
flowchart LR
subgraph Admin[Administrative Ebene]
Land[Land
Schweiz]
Kanton[Kanton
z.B. Zürich]
Gemeinde[Gemeinde
z.B. Zürich Stadt]
Land --> Kanton
Kanton --> Gemeinde
end
subgraph Geo[Geografische Daten]
GeoPolylinie[GeoPolylinie
Linie/Polygon]
GeoPunkt[GeoPunkt
Koordinaten]
GeoPolylinie --> GeoPunkt
end
subgraph Core[Kern-Business-Logik]
Projekt[Projekt
Bauprojekt]
Parzelle[Parzelle
Grundstück mit
Bauparametern]
Gemeinde --> Parzelle
Projekt --> Parzelle
Projekt --> GeoPolylinie
Parzelle --> GeoPolylinie
end
subgraph Support[Unterstützende Daten]
Dokument[Dokument
Dateien & URLs]
Kontext[Kontext
Zusatzinfos]
end
```
### Übersichtstabelle aller Entitäten
| Objekt | Typ | Beschreibung | Hauptfelder |
|--------|-----|--------------|-------------|
| **Projekt** | Kernentität | Bauprojekt mit Status und Perimeter | id, label, statusProzess, perimeter, baulinie, parzellen |
| **Parzelle** | Hauptentität | Grundstück mit Bauparametern | id, label, plz, bauzone, AZ, BZ, perimeter, baulinie, laermschutzzone, hochwasserschutzzone, grundwasserschutzzone |
| **Dokument** | Unterstützend | Dateien und URLs mit Versionierung | id, label, dokumentTyp, quelle, mimeType, kategorienTags |
| **Kontext** | Unterstützend | Flexible Zusatzinformationen | id, thema, inhalt |
| **GeoPolylinie** | Hilfsobjekt | Geometrische Linie/Polygon | id, closed, punkte |
| **Land** | Admin | Nationale Ebene | id, label, abk |
| **Kanton** | Admin | Kantonale Ebene mit Baurecht | id, label, id_land, abk |
| **Gemeinde** | Admin | Gemeinde-Ebene mit BZO | id, label, id_kanton, plz |
| **GeoPunkt** | Hilfsobjekt | 3D-Koordinate | koordinatensystem, x, y, z, referenz |
| **GeoTag** | Enum | Geopunkt-Kategorien | K1, K2, K3, Geometer |
| **JaNein** | Enum | Drei-wertiger Status | "", "Ja", "Nein" |
| **StatusProzess** | Enum | Projektstatus | 7 Werte (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv) |
| **DokumentTyp** | Enum | Dokumenttyp | 6 Werte (kantonBaureglementAktuell, kantonBaureglementRevision, etc.) |
### Beispiel: Vollständige Pydantic-Modelle für Real Estate-Entitäten
Hier ist ein Beispiel, wie die Pydantic-Modelle für die Real Estate-Entitäten aussehen sollten:
```python
"""
Real Estate data models for Architektur-Planungs-App.
Implements a general Swiss architecture planning data model.
(PEK is one example implementation, but the model is general-purpose)
"""
from typing import List, Dict, Any, Optional, ForwardRef
from enum import Enum
from pydantic import BaseModel, Field
from modules.shared.attributeUtils import registerModelLabels
from modules.shared.timeUtils import getUtcTimestamp
import uuid
# ===== Enums =====
class StatusProzess(str, Enum):
"""Project process status"""
EINGANG = "Eingang"
ANALYSE = "Analyse"
STUDIE = "Studie"
PLANUNG = "Planung"
BAURECHTSVERFAHREN = "Baurechtsverfahren"
UMSETZUNG = "Umsetzung"
ARCHIV = "Archiv"
class DokumentTyp(str, Enum):
"""Document type for categorization"""
KANTON_BAUREGLEMENT_AKTUELL = "kantonBaureglementAktuell"
KANTON_BAUREGLEMENT_REVISION = "kantonBaureglementRevision"
KANTON_BAUVERORDNUNG_AKTUELL = "kantonBauverordnungAktuell"
KANTON_BAUVERORDNUNG_REVISION = "kantonBauverordnungRevision"
GEMEINDE_BZO_AKTUELL = "gemeindeBzoAktuell"
GEMEINDE_BZO_REVISION = "gemeindeBzoRevision"
class JaNein(str, Enum):
"""Three-valued state for optional yes/no questions"""
UNBEKANNT = "" # Empty string for unknown/not captured
JA = "Ja"
NEIN = "Nein"
class GeoTag(str, Enum):
"""Geopoint categories"""
K1 = "K1" # Fixpunkt höchster Genauigkeit
K2 = "K2" # Fixpunkt mittlerer Genauigkeit
K3 = "K3" # Fixpunkt niedriger Genauigkeit
GEOMETER = "Geometer" # Vom Geometer vermessener Punkt
# ===== Helper Models (must be defined before main models) =====
class GeoPunkt(BaseModel):
"""Represents a 3D point with reference."""
koordinatensystem: str = Field(
description="Coordinate system (e.g. 'LV95', 'EPSG:2056')",
frontend_type="text",
frontend_readonly=False,
frontend_required=True,
)
x: float = Field(
description="East value (E) [m], typically 2'480'000 - 2'840'000",
frontend_type="number",
frontend_readonly=False,
frontend_required=True,
)
y: float = Field(
description="North value (N) [m], typically 1'070'000 - 1'300'000",
frontend_type="number",
frontend_readonly=False,
frontend_required=True,
)
z: Optional[float] = Field(
None,
description="Height above sea level [m]",
frontend_type="number",
frontend_readonly=False,
frontend_required=False,
)
referenz: Optional[GeoTag] = Field(
None,
description="Point categorization",
frontend_type="select",
frontend_readonly=False,
frontend_required=False,
)
class GeoPolylinie(BaseModel):
"""Represents a line or polygon from multiple GeoPunkte."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
)
closed: bool = Field(
description="Is the GeoPolylinie closed (polygon)?",
frontend_type="boolean",
frontend_readonly=False,
frontend_required=True,
)
punkte: List[GeoPunkt] = Field(
default_factory=list,
description="List of GeoPunkte forming the GeoPolylinie",
frontend_type="json",
frontend_readonly=False,
frontend_required=True,
)
class Dokument(BaseModel):
"""Supporting data object for file and URL management with versioning."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
mandateId: str = Field(
description="ID of the mandate this document belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
label: str = Field(
description="Document label",
frontend_type="text",
frontend_readonly=False,
frontend_required=True,
)
versionsbezeichnung: Optional[str] = Field(
None,
description="Version number or designation (e.g. 'v1.0', 'Rev. A')",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
dokumentTyp: Optional[DokumentTyp] = Field(
None,
description="Document type",
frontend_type="select",
frontend_readonly=False,
frontend_required=False,
)
dokumentReferenz: str = Field(
description="File path or URL",
frontend_type="text",
frontend_readonly=False,
frontend_required=True,
)
quelle: Optional[str] = Field(
None,
description="Source of the document",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
mimeType: Optional[str] = Field(
None,
description="MIME type of the document (e.g. 'application/pdf', 'image/png')",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
kategorienTags: List[str] = Field(
default_factory=list,
description="Document categorization tags",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
# Beispiel-Kategorien für Dokumente (nicht abschließend):
# - "Kataster Objekte" - Amtliche Vermessung
# - "Kataster Werkeleitungen" - Leitungskataster
# - "Kataster Belastete Standorte" - Altlasten
# - "Kataster Bäume" - Baumkataster
# - "Zonenplan" - Zonenpläne
# - "Planungs- und Baugesetz (PGB)" - Kantonale Baugesetze
# - "Bau- und Zonenordnung (BZO)" - Gemeinde BZO
# - "Parkplatzverordnung" - Parkplatzregelungen
# - "Eigentümerauskunft" - Grundbuch-Auszüge Eigentümer
# - "Grundbuchauszug" - Vollständige Grundbuch-Auszüge
# - "Bauherrschaft" - Dokumente von der Bauherrschaft
# - "Planung" - Planungsdokumente
#
# Hinweis: Aktuelle Dokumente (z.B. aktuelle Baureglemente, BZO) können anhand des
# `dokumentTyp`-Attributs identifiziert werden. Die entsprechenden Dokumente finden sich
# in der `dokumente`-Liste der jeweiligen Entität (Kanton, Gemeinde).
class Kontext(BaseModel):
"""Supporting data object for flexible additional information."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
)
thema: str = Field(
description="Theme designation",
frontend_type="text",
frontend_readonly=False,
frontend_required=True,
)
inhalt: str = Field(
description="Detailed information (text)",
frontend_type="textarea",
frontend_readonly=False,
frontend_required=True,
)
# Beispielthemen für Kontext (nicht abschließend):
# - "Nutzung" - Vorgaben zur Erdgeschossnutzung (Wohnen erlaubt oder Pflicht für Gewerbe)
# - "Rechte" - Dienstbarkeiten (Wegrechte, Nähebaurechte, etc.)
# - "Parkierung" - Anforderung Parkplätze (Berechnung / Reduktionsfaktoren)
# - "Ausnützung" - Ausnützungsübertragungen
# - "Umwelt" - Schadstoffbelastungen auf Parzellen
# - "Planung" - Aktive Gestaltungspläne
# - "Lärm" - Lärmempfindlichkeitsstufen
# - "Energie" - Mögliche Wärmenutzung (Wärmeverbundnetze; Fernwärme, Anergie)
# - "Natur" - Baumbestand auf privaten Grundstücken
# - "Schutz" - Isos (Ortsbild, Schutzstatus, Denkmalschutz, Weilergebiet, etc.)
# - "Gefahren" - Naturgefahren (z.B. Objektschutzmassnahmen (Hochwasser))
# - "Revision" - Verweis auf aktuell in oder zukünftig in Revision befindlichen Normen/Gesetze
#
# Verwendung: Kontext-Objekte werden als Listen in den jeweiligen Entitäten gespeichert:
# - projekt.kontextInformationen: list[Kontext]
# - parzelle.kontextInformationen: list[Kontext]
# - land.kontextInformationen: list[Kontext]
# - kanton.kontextInformationen: list[Kontext]
# - gemeinde.kontextInformationen: list[Kontext]
#
# Design-Rationale: Das Kontext-Objekt ermöglicht flexibles Hinzufügen von projektspezifischen,
# parzellen-spezifischen oder regionalen Informationen ohne Schemaänderungen.
class Land(BaseModel):
"""National level administrative entity."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
mandateId: str = Field(
description="ID of the mandate",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
label: str = Field(
description="Country name (e.g. 'Schweiz')",
frontend_type="text",
frontend_readonly=False,
frontend_required=True,
)
abk: Optional[str] = Field(
None,
description="Abbreviation (e.g. 'CH')",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
dokumente: List[Dokument] = Field(
default_factory=list,
description="National laws/documents",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
kontextInformationen: List[Kontext] = Field(
default_factory=list,
description="National context information",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
class Kanton(BaseModel):
"""Cantonal level administrative entity."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
mandateId: str = Field(
description="ID of the mandate",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
label: str = Field(
description="Canton name (e.g. 'Zürich')",
frontend_type="text",
frontend_readonly=False,
frontend_required=True,
)
id_land: Optional[str] = Field(
None,
description="Land ID (Foreign Key) - eindeutiger Link zum Land, in welchem Land der Kanton liegt",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
abk: Optional[str] = Field(
None,
description="Abbreviation (e.g. 'ZH')",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
dokumente: List[Dokument] = Field(
default_factory=list,
description="Cantonal documents",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
kontextInformationen: List[Kontext] = Field(
default_factory=list,
description="Canton-specific context information",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
class Gemeinde(BaseModel):
"""Municipal level administrative entity."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
mandateId: str = Field(
description="ID of the mandate",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
label: str = Field(
description="Municipality name (e.g. 'Zürich')",
frontend_type="text",
frontend_readonly=False,
frontend_required=True,
)
id_kanton: Optional[str] = Field(
None,
description="Kanton ID (Foreign Key) - eindeutiger Link zum Kanton, in welchem Kanton die Gemeinde liegt",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
plz: Optional[str] = Field(
None,
description="Postal code (for municipalities with multiple PLZ, this can be a main PLZ). Bei Gemeinden mit mehreren Postleitzahlen wird die konkrete PLZ der Parzelle im Attribut `plz` der Parzelle erfasst.",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
dokumente: List[Dokument] = Field(
default_factory=list,
description="Municipal documents",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
kontextInformationen: List[Kontext] = Field(
default_factory=list,
description="Municipality-specific context information",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
# ===== Main Models (use ForwardRef for circular references) =====
# Forward references for circular dependencies
ParzelleRef = ForwardRef('Parzelle')
class Parzelle(BaseModel):
"""Represents a plot with all building law properties."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
mandateId: str = Field(
description="ID of the mandate",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
# Grunddaten
label: str = Field(
description="Plot designation",
frontend_type="text",
frontend_readonly=False,
frontend_required=True,
)
parzellenAliasTags: List[str] = Field(
default_factory=list,
description="Additional plot names or field names",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
eigentuemerschaft: Optional[str] = Field(
None,
description="Owner of the plot",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
strasseNr: Optional[str] = Field(
None,
description="Street and house number",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
plz: Optional[str] = Field(
None,
description="Postal code of the plot",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
# Geografischer Kontext
perimeter: Optional[GeoPolylinie] = Field(
None,
description="Plot boundary as closed GeoPolylinie",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
baulinie: Optional[GeoPolylinie] = Field(
None,
description="Building line of the plot",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
kontextLand: Optional[str] = Field(
None,
description="Land ID (Foreign Key)",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
kontextKanton: Optional[str] = Field(
None,
description="Canton ID (Foreign Key)",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
kontextGemeinde: Optional[str] = Field(
None,
description="Municipality ID (Foreign Key)",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
# Bebauungsparameter
bauzone: Optional[str] = Field(
None,
description="Building zone designation (e.g. W3, WG2, etc.)",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
az: Optional[float] = Field(
None,
description="Ausnützungsziffer",
frontend_type="number",
frontend_readonly=False,
frontend_required=False,
)
bz: Optional[float] = Field(
None,
description="Bebauungsziffer",
frontend_type="number",
frontend_readonly=False,
frontend_required=False,
)
vollgeschossZahl: Optional[int] = Field(
None,
description="Number of allowed full floors",
frontend_type="number",
frontend_readonly=False,
frontend_required=False,
)
anrechenbarDachgeschoss: Optional[float] = Field(
None,
description="Accountable portion of attic (0.0 - 1.0)",
frontend_type="number",
frontend_readonly=False,
frontend_required=False,
)
anrechenbarUntergeschoss: Optional[float] = Field(
None,
description="Accountable portion of basement (0.0 - 1.0)",
frontend_type="number",
frontend_readonly=False,
frontend_required=False,
)
gebaeudehoeheMax: Optional[float] = Field(
None,
description="Maximum building height in meters",
frontend_type="number",
frontend_readonly=False,
frontend_required=False,
)
# Abstandsregelungen
regelnGrenzabstand: List[str] = Field(
default_factory=list,
description="Regulations for boundary distance",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
regelnMehrlaengenzuschlag: List[str] = Field(
default_factory=list,
description="Regulations for additional length surcharge",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
regelnMehrhoehenzuschlag: List[str] = Field(
default_factory=list,
description="Regulations for additional height surcharge",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
# Eigenschaften (Ja/Nein)
parzelleBebaut: Optional[JaNein] = Field(
None,
description="Is the plot built?",
frontend_type="select",
frontend_readonly=False,
frontend_required=False,
)
parzelleErschlossen: Optional[JaNein] = Field(
None,
description="Is the plot developed?",
frontend_type="select",
frontend_readonly=False,
frontend_required=False,
)
parzelleHanglage: Optional[JaNein] = Field(
None,
description="Is the plot on a slope?",
frontend_type="select",
frontend_readonly=False,
frontend_required=False,
)
# Schutzzonen
laermschutzzone: Optional[str] = Field(
None,
description="Noise protection zone (e.g. 'II')",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
hochwasserschutzzone: Optional[str] = Field(
None,
description="Flood protection zone (e.g. 'tief')",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
grundwasserschutzzone: Optional[str] = Field(
None,
description="Groundwater protection zone",
frontend_type="text",
frontend_readonly=False,
frontend_required=False,
)
# Beziehungen (stored as JSONB in database)
parzellenNachbarschaft: List[Dict[str, Any]] = Field(
default_factory=list,
description="Neighboring plots (stored as list of Parzelle IDs or full objects)",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
dokumente: List[Dokument] = Field(
default_factory=list,
description="Plot-specific documents",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
kontextInformationen: List[Kontext] = Field(
default_factory=list,
description="Plot-specific context information",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
class Projekt(BaseModel):
"""Core object representing a construction project."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
mandateId: str = Field(
description="ID of the mandate",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
label: str = Field(
description="Project designation",
frontend_type="text",
frontend_readonly=False,
frontend_required=True,
)
statusProzess: Optional[StatusProzess] = Field(
None,
description="Project status",
frontend_type="select",
frontend_readonly=False,
frontend_required=False,
)
perimeter: Optional[GeoPolylinie] = Field(
None,
description="Envelope of all plots in the project",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
baulinie: Optional[GeoPolylinie] = Field(
None,
description="Building line of the project",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
parzellen: List[Parzelle] = Field(
default_factory=list,
description="All plots of the project",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
dokumente: List[Dokument] = Field(
default_factory=list,
description="Project-specific documents",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
kontextInformationen: List[Kontext] = Field(
default_factory=list,
description="Project-specific context information",
frontend_type="json",
frontend_readonly=False,
frontend_required=False,
)
# Resolve forward references
Parzelle.model_rebuild()
Projekt.model_rebuild()
# Register labels for frontend
registerModelLabels(
"Projekt",
{"en": "Project", "fr": "Projet", "de": "Projekt"},
{
"id": {"en": "ID", "fr": "ID", "de": "ID"},
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
"statusProzess": {"en": "Process Status", "fr": "Statut du processus", "de": "Prozessstatus"},
# ... more labels
},
)
# Similar registerModelLabels calls for all other models...
```
**Wichtige Hinweise zur Implementierung:**
1. **Forward References**: Für zirkuläre Referenzen (z.B. `Parzelle` → `parzellenNachbarschaft: list[Parzelle]`) verwenden Sie `ForwardRef` oder speichern Sie nur IDs als Strings.
2. **JSONB-Speicherung**: Listen von Objekten (`list[Parzelle]`, `list[Dokument]`) werden automatisch als JSONB gespeichert. Der DatabaseConnector erkennt `List`-Typen automatisch.
3. **Foreign Keys**: Einzelne Objektreferenzen wie `kontextKanton: Optional[str]` werden als String-ID gespeichert. Sie können später im Interface die vollständigen Objekte laden.
4. **MandateId**: Alle Entitäten benötigen `mandateId` für Mandaten-Isolation.
5. **Systemattribute**: `_createdAt`, `_createdBy`, `_modifiedAt`, `_modifiedBy` werden automatisch vom DatabaseConnector hinzugefügt - Sie müssen sie nicht im Modell definieren.
---
**WICHTIG:** Die obigen Real Estate-Modelle (`Projekt`, `Parzelle`, etc.) sind die **tatsächlichen Datenmodelle**, die Sie implementieren müssen. Diese werden in `modules/datamodels/datamodelRealEstate.py` erstellt.
**Keine Chat-Interface-Modelle notwendig:**
- Das Feature arbeitet **stateless** ohne Session-Management
- Alle Operationen arbeiten direkt auf den Real Estate-Modellen
- Keine `RealEstateChatSession`, `RealEstateQuery` oder `RealEstateQueryResult` notwendig
- CRUD-Operationen werden direkt ausgeführt und Ergebnisse direkt zurückgegeben
### Wichtige Punkte:
1. **UUID als ID**: Alle Modelle verwenden `uuid.uuid4()` für eindeutige IDs
2. **MandateId**: Jedes Modell benötigt `mandateId` für Mandaten-Isolation
3. **Frontend-Metadaten**: `frontend_type`, `frontend_readonly`, `frontend_required` für UI-Generierung
4. **registerModelLabels**: Registriert Labels für Mehrsprachigkeit
5. **JSONB-Felder**: `Dict[str, Any]` und `List[...]` werden automatisch als JSONB in PostgreSQL gespeichert
6. **Foreign Keys**: Administrative Hierarchie wird über Foreign Keys abgebildet:
- `Kanton.id_land` → `Land.id`
- `Gemeinde.id_kanton` → `Kanton.id`
- `Parzelle.kontextLand` → `Land.id` (Optional)
- `Parzelle.kontextKanton` → `Kanton.id` (Optional)
- `Parzelle.kontextGemeinde` → `Gemeinde.id` (Optional)
---
## Q & A - Häufige Fragen
1. **Versionierung**: Sollen Änderungen an Parzellen historisiert werden?
→ Vorerst nicht
2. **Mehrsprachigkeit**: Labels in DE/FR/IT?
→ Wird im Pydantic Model über `registerModelLabels` umgesetzt
3. **Benutzer & Rollen**: Wer darf was bearbeiten?
→ In der App über Roles und Permissions gesteuert (UAM-System)
4. **Workflow-Engine**: Für Statusübergänge und Genehmigungen?
→ In der App über Workflow-Engine gesteuert (optional, kann später integriert werden)
5. **Integration**: Anbindung an amtliche Geodaten (z.B. Swisstopo API)?
→ In der App über Integrationen gesteuert (optional)
6. **Berechnungen**: Sollen Ausnützungsberechnungen automatisiert werden?
→ In der App über Berechnungen gesteuert (optional)
---
[← Zurück zur Übersicht](README.md) | [Weiter: Interface erstellen →](03-interfaces.md)