Frist part of config. Added standard kpi config data. Added integrating from back- and frontend.

pull/55/head
s8613 2025-06-05 15:08:44 +02:00
parent 5d5bc986cf
commit 59c918cdce
6 changed files with 328 additions and 29 deletions

View File

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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -16,4 +16,6 @@ RUN python -m spacy download en_core_web_sm
COPY .. /app
ENV PYTHONUNBUFFERED=1
CMD ["python3.12", "app.py"]

View File

@ -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, string> = {
"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<Kennzahl[]>(mockKennzahlen);
const [kennzahlen, setKennzahlen] = useState<Kennzahl[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [draggedItem, setDraggedItem] = useState<Kennzahl | null>(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<HTMLTableRowElement>, item: Kennzahl) => {
@ -76,6 +120,62 @@ function ConfigPage() {
setDraggedItem(null);
};
if (loading) {
return (
<Box
minHeight="100vh"
width="100vw"
bgcolor="white"
display="flex"
justifyContent="center"
alignItems="center"
flexDirection="column"
>
<CircularProgress sx={{ color: '#383838', mb: 2 }} />
<Typography>Lade Kennzahlen...</Typography>
</Box>
);
}
if (error) {
return (
<Box
minHeight="100vh"
width="100vw"
bgcolor="white"
display="flex"
justifyContent="center"
alignItems="center"
flexDirection="column"
>
<Typography color="error" mb={2} variant="h6">
Fehler beim Laden der Daten
</Typography>
<Typography color="text.secondary" mb={3} textAlign="center" maxWidth="500px">
{error}
</Typography>
<Box display="flex" gap={2}>
<Button
variant="outlined"
onClick={() => navigate({ to: "/" })}
>
Zurück
</Button>
<Button
variant="contained"
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
onClick={() => window.location.reload()}
>
Neu laden
</Button>
</Box>
</Box>
);
}
return (
<Box
minHeight="100vh"
@ -233,7 +333,7 @@ function ConfigPage() {
border: "1px solid #ddd",
backgroundColor: "#f8f9fa"
}}>
{kennzahl.format}
{getDisplayType(kennzahl.type)}
</span>
</td>
</tr>