# 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)