diff --git a/project/backend/coordinator/controller/kpi_setting_controller.py b/project/backend/coordinator/controller/kpi_setting_controller.py index 5a9fb40..4f48cb2 100644 --- a/project/backend/coordinator/controller/kpi_setting_controller.py +++ b/project/backend/coordinator/controller/kpi_setting_controller.py @@ -34,6 +34,8 @@ def create_kpi_setting(): "type", "translation", "example", + "position", + "active" ] for field in required_fields: if field not in data: @@ -58,6 +60,8 @@ def create_kpi_setting(): type=kpi_type, translation=data["translation"], example=data["example"], + position=data["position"], + active=data["active"] ) db.session.add(new_kpi_setting) diff --git a/project/backend/coordinator/model/database.py b/project/backend/coordinator/model/database.py index 1aec7b3..1d350de 100644 --- a/project/backend/coordinator/model/database.py +++ b/project/backend/coordinator/model/database.py @@ -13,3 +13,5 @@ def init_db(app): db.init_app(app) with app.app_context(): db.create_all() + from model.seed_data import seed_default_kpi_settings + seed_default_kpi_settings() diff --git a/project/backend/coordinator/model/kpi_setting_model.py b/project/backend/coordinator/model/kpi_setting_model.py index f3775a0..496fa8f 100644 --- a/project/backend/coordinator/model/kpi_setting_model.py +++ b/project/backend/coordinator/model/kpi_setting_model.py @@ -10,6 +10,7 @@ class KPISettingType(Enum): RANGE = "range" BOOLEAN = "boolean" ARRAY = "array" + DATE = "date" class KPISettingModel(db.Model): @@ -24,6 +25,8 @@ class KPISettingModel(db.Model): ) translation: Mapped[str] example: Mapped[str] + position: Mapped[int] + active: Mapped[bool] def to_dict(self): return { @@ -34,12 +37,16 @@ class KPISettingModel(db.Model): "type": self.type.value, "translation": self.translation, "example": self.example, + "position": self.position, + "active": self.active } - def __init__(self, name, description, mandatory, type, translation, example): + def __init__(self, name, description, mandatory, type, translation, example, position, active): self.name = name self.description = description self.mandatory = mandatory self.type = type self.translation = translation self.example = example + self.position = position + self.active = active diff --git a/project/backend/coordinator/model/seed_data.py b/project/backend/coordinator/model/seed_data.py new file mode 100644 index 0000000..30b50a3 --- /dev/null +++ b/project/backend/coordinator/model/seed_data.py @@ -0,0 +1,184 @@ +from model.database import db +from model.kpi_setting_model import KPISettingModel, KPISettingType + +def seed_default_kpi_settings(): + if KPISettingModel.query.first() is not None: + print("KPI Settings bereits vorhanden, Seeding übersprungen") + return + + default_kpi_settings = [ + { + "name": "Fondsname", + "description": "Der vollständige Name des Investmentfonds", + "mandatory": True, + "type": KPISettingType.STRING, + "translation": "Fund Name", + "example": "Alpha Real Estate Fund I", + "position": 1, + "active": True + }, + { + "name": "Fondsmanager", + "description": "Verantwortlicher Manager für die Fondsverwaltung", + "mandatory": True, + "type": KPISettingType.STRING, + "translation": "Fund Manager", + "example": "Max Mustermann", + "position": 2, + "active": True + }, + { + "name": "AIFM", + "description": "Alternative Investment Fund Manager", + "mandatory": True, + "type": KPISettingType.STRING, + "translation": "AIFM", + "example": "Alpha Investment Management GmbH", + "position": 3, + "active": True + }, + { + "name": "Datum", + "description": "Stichtag der Datenerfassung", + "mandatory": True, + "type": KPISettingType.DATE, + "translation": "Date", + "example": "05.05.2025", + "position": 4, + "active": True + }, + { + "name": "Risikoprofil", + "description": "Klassifizierung des Risikos des Fonds", + "mandatory": True, + "type": KPISettingType.STRING, + "translation": "Risk Profile", + "example": "Core/Core++", + "position": 5, + "active": True + }, + { + "name": "Artikel", + "description": "Artikel 8 SFDR-Klassifizierung", + "mandatory": False, + "type": KPISettingType.BOOLEAN, + "translation": "Article", + "example": "Ja", + "position": 6, + "active": True + }, + { + "name": "Zielrendite", + "description": "Angestrebte jährliche Rendite in Prozent", + "mandatory": True, + "type": KPISettingType.NUMBER, + "translation": "Target Return", + "example": "6.5", + "position": 7, + "active": True + }, + { + "name": "Rendite", + "description": "Tatsächlich erzielte Rendite in Prozent", + "mandatory": False, + "type": KPISettingType.NUMBER, + "translation": "Return", + "example": "5.8", + "position": 8, + "active": True + }, + { + "name": "Zielausschüttung", + "description": "Geplante Ausschüttung in Prozent", + "mandatory": False, + "type": KPISettingType.NUMBER, + "translation": "Target Distribution", + "example": "4.0", + "position": 9, + "active": True + }, + { + "name": "Ausschüttung", + "description": "Tatsächliche Ausschüttung in Prozent", + "mandatory": False, + "type": KPISettingType.NUMBER, + "translation": "Distribution", + "example": "3.8", + "position": 10, + "active": True + }, + { + "name": "Laufzeit", + "description": "Geplante Laufzeit des Fonds", + "mandatory": True, + "type": KPISettingType.STRING, + "translation": "Duration", + "example": "7 Jahre", + "position": 11, + "active": True + }, + { + "name": "LTV", + "description": "Loan-to-Value Verhältnis in Prozent", + "mandatory": False, + "type": KPISettingType.NUMBER, + "translation": "LTV", + "example": "65.0", + "position": 12, + "active": True + }, + { + "name": "Managementgebühren", + "description": "Jährliche Verwaltungsgebühren in Prozent", + "mandatory": True, + "type": KPISettingType.NUMBER, + "translation": "Management Fees", + "example": "1.5", + "position": 13, + "active": True + }, + { + "name": "Sektorenallokation", + "description": "Verteilung der Investments nach Sektoren", + "mandatory": False, + "type": KPISettingType.ARRAY, + "translation": "Sector Allocation", + "example": "Büroimmobilien, Einzelhandel, Logistik", + "position": 14, + "active": True + }, + { + "name": "Länderallokation", + "description": "Geografische Verteilung der Investments", + "mandatory": False, + "type": KPISettingType.ARRAY, + "translation": "Country Allocation", + "example": "Deutschland, Österreich, Schweiz", + "position": 15, + "active": True + } + ] + + print("Füge Standard KPI Settings hinzu...") + + for kpi_data in default_kpi_settings: + kpi_setting = KPISettingModel( + name=kpi_data["name"], + description=kpi_data["description"], + mandatory=kpi_data["mandatory"], + type=kpi_data["type"], + translation=kpi_data["translation"], + example=kpi_data["example"], + position=kpi_data["position"], + active=kpi_data["active"] + ) + + db.session.add(kpi_setting) + + try: + db.session.commit() + print(f"Erfolgreich {len(default_kpi_settings)} Standard KPI Settings hinzugefügt") + except Exception as e: + db.session.rollback() + print(f"Fehler beim Hinzufügen der Standard KPI Settings: {e}") + raise \ No newline at end of file diff --git a/project/backend/spacy-service/Dockerfile b/project/backend/spacy-service/Dockerfile index 6b5dcde..d7ad008 100644 --- a/project/backend/spacy-service/Dockerfile +++ b/project/backend/spacy-service/Dockerfile @@ -16,4 +16,6 @@ RUN python -m spacy download en_core_web_sm COPY .. /app +ENV PYTHONUNBUFFERED=1 + CMD ["python3.12", "app.py"] diff --git a/project/frontend/src/routes/config.tsx b/project/frontend/src/routes/config.tsx index 6ba17e6..29eea58 100644 --- a/project/frontend/src/routes/config.tsx +++ b/project/frontend/src/routes/config.tsx @@ -1,9 +1,9 @@ import { createFileRoute } from "@tanstack/react-router"; -import { Box, Button, IconButton, Typography } from "@mui/material"; +import { Box, Button, IconButton, Typography, CircularProgress } from "@mui/material"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; import { useNavigate } from "@tanstack/react-router"; -import { useState } from "react"; +import { useEffect, useState } from "react"; export const Route = createFileRoute("/config")({ component: ConfigPage, @@ -12,39 +12,83 @@ export const Route = createFileRoute("/config")({ interface Kennzahl { id: number; name: string; - format: string; + description: string; + mandatory: boolean; + type: string; + translation: string; + example: string; + position: number; active: boolean; } -const mockKennzahlen: Kennzahl[] = [ - { id: 1, name: "Fondsname", format: "Text", active: true }, - { id: 2, name: "Fondsmanager", format: "Text", active: true }, - { id: 3, name: "AIFM", format: "Text", active: false }, - { id: 4, name: "Datum", format: "Datum", active: true }, - { id: 5, name: "Risikoprofil", format: "Text", active: true }, - { id: 6, name: "Artikel", format: "Ja/Nein", active: true }, - { id: 7, name: "Zielrendite", format: "Zahl", active: true }, - { id: 8, name: "Rendite", format: "Zahl", active: true }, - { id: 9, name: "Zielausschüttung", format: "Zahl", active: true }, - { id: 10, name: "Ausschüttung", format: "Zahl", active: true }, - { id: 11, name: "Laufzeit", format: "Text", active: true }, - { id: 12, name: "LTV", format: "Zahl", active: true }, - { id: 13, name: "Managementgebühren", format: "Zahl", active: true }, - { id: 14, name: "Sektorenallokation", format: "Liste (mehrfach)", active: true }, - { id: 15, name: "Länderallokation", format: "Liste (mehrfach)", active: true }, -]; +const typeDisplayMapping: Record = { + "string": "Text", + "date": "Datum", + "boolean": "Ja/Nein", + "number": "Zahl", + "array": "Liste (mehrfach)" +}; + +const getDisplayType = (backendType: string): string => { + return typeDisplayMapping[backendType] || backendType; +}; function ConfigPage() { const navigate = useNavigate(); - const [kennzahlen, setKennzahlen] = useState(mockKennzahlen); + const [kennzahlen, setKennzahlen] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); const [draggedItem, setDraggedItem] = useState(null); - const handleToggleActive = (id: number) => { - setKennzahlen(prev => - prev.map(item => - item.id === id ? { ...item, active: !item.active } : item - ) - ); + useEffect(() => { + const fetchKennzahlen = async () => { + try { + const response = await fetch(`http://localhost:5050/api/kpi_setting/`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + const sortedData = data.sort((a: Kennzahl, b: Kennzahl) => a.position - b.position); + setKennzahlen(sortedData); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unbekannter Fehler beim Laden der Daten'); + } finally { + setLoading(false); + } + }; + + fetchKennzahlen(); + }, []); + + const handleToggleActive = async (id: number) => { + const kennzahl = kennzahlen.find(k => k.id === id); + if (!kennzahl) return; + + try { + const response = await fetch(`http://localhost:5050/api/kpi_setting/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...kennzahl, + active: !kennzahl.active + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + setKennzahlen(prev => + prev.map(item => + item.id === id ? { ...item, active: !item.active } : item + ) + ); + } catch (err) { + console.error('Fehler beim Aktualisieren der Kennzahl:', err); + } }; const handleDragStart = (e: React.DragEvent, item: Kennzahl) => { @@ -76,6 +120,62 @@ function ConfigPage() { setDraggedItem(null); }; + if (loading) { + return ( + + + Lade Kennzahlen... + + ); + } + + if (error) { + return ( + + + Fehler beim Laden der Daten + + + {error} + + + + + + + ); + } + return ( - {kennzahl.format} + {getDisplayType(kennzahl.type)}