Frist part of config. Added standard kpi config data. Added integrating from back- and frontend.
parent
5d5bc986cf
commit
59c918cdce
|
|
@ -34,6 +34,8 @@ def create_kpi_setting():
|
||||||
"type",
|
"type",
|
||||||
"translation",
|
"translation",
|
||||||
"example",
|
"example",
|
||||||
|
"position",
|
||||||
|
"active"
|
||||||
]
|
]
|
||||||
for field in required_fields:
|
for field in required_fields:
|
||||||
if field not in data:
|
if field not in data:
|
||||||
|
|
@ -58,6 +60,8 @@ def create_kpi_setting():
|
||||||
type=kpi_type,
|
type=kpi_type,
|
||||||
translation=data["translation"],
|
translation=data["translation"],
|
||||||
example=data["example"],
|
example=data["example"],
|
||||||
|
position=data["position"],
|
||||||
|
active=data["active"]
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(new_kpi_setting)
|
db.session.add(new_kpi_setting)
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,5 @@ def init_db(app):
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
from model.seed_data import seed_default_kpi_settings
|
||||||
|
seed_default_kpi_settings()
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ class KPISettingType(Enum):
|
||||||
RANGE = "range"
|
RANGE = "range"
|
||||||
BOOLEAN = "boolean"
|
BOOLEAN = "boolean"
|
||||||
ARRAY = "array"
|
ARRAY = "array"
|
||||||
|
DATE = "date"
|
||||||
|
|
||||||
|
|
||||||
class KPISettingModel(db.Model):
|
class KPISettingModel(db.Model):
|
||||||
|
|
@ -24,6 +25,8 @@ class KPISettingModel(db.Model):
|
||||||
)
|
)
|
||||||
translation: Mapped[str]
|
translation: Mapped[str]
|
||||||
example: Mapped[str]
|
example: Mapped[str]
|
||||||
|
position: Mapped[int]
|
||||||
|
active: Mapped[bool]
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
|
|
@ -34,12 +37,16 @@ class KPISettingModel(db.Model):
|
||||||
"type": self.type.value,
|
"type": self.type.value,
|
||||||
"translation": self.translation,
|
"translation": self.translation,
|
||||||
"example": self.example,
|
"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.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
self.mandatory = mandatory
|
self.mandatory = mandatory
|
||||||
self.type = type
|
self.type = type
|
||||||
self.translation = translation
|
self.translation = translation
|
||||||
self.example = example
|
self.example = example
|
||||||
|
self.position = position
|
||||||
|
self.active = active
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -16,4 +16,6 @@ RUN python -m spacy download en_core_web_sm
|
||||||
|
|
||||||
COPY .. /app
|
COPY .. /app
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
CMD ["python3.12", "app.py"]
|
CMD ["python3.12", "app.py"]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
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 ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
|
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/config")({
|
export const Route = createFileRoute("/config")({
|
||||||
component: ConfigPage,
|
component: ConfigPage,
|
||||||
|
|
@ -12,39 +12,83 @@ export const Route = createFileRoute("/config")({
|
||||||
interface Kennzahl {
|
interface Kennzahl {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
format: string;
|
description: string;
|
||||||
|
mandatory: boolean;
|
||||||
|
type: string;
|
||||||
|
translation: string;
|
||||||
|
example: string;
|
||||||
|
position: number;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockKennzahlen: Kennzahl[] = [
|
const typeDisplayMapping: Record<string, string> = {
|
||||||
{ id: 1, name: "Fondsname", format: "Text", active: true },
|
"string": "Text",
|
||||||
{ id: 2, name: "Fondsmanager", format: "Text", active: true },
|
"date": "Datum",
|
||||||
{ id: 3, name: "AIFM", format: "Text", active: false },
|
"boolean": "Ja/Nein",
|
||||||
{ id: 4, name: "Datum", format: "Datum", active: true },
|
"number": "Zahl",
|
||||||
{ id: 5, name: "Risikoprofil", format: "Text", active: true },
|
"array": "Liste (mehrfach)"
|
||||||
{ 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 },
|
const getDisplayType = (backendType: string): string => {
|
||||||
{ id: 9, name: "Zielausschüttung", format: "Zahl", active: true },
|
return typeDisplayMapping[backendType] || backendType;
|
||||||
{ 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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
function ConfigPage() {
|
function ConfigPage() {
|
||||||
const navigate = useNavigate();
|
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 [draggedItem, setDraggedItem] = useState<Kennzahl | null>(null);
|
||||||
|
|
||||||
const handleToggleActive = (id: number) => {
|
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 =>
|
setKennzahlen(prev =>
|
||||||
prev.map(item =>
|
prev.map(item =>
|
||||||
item.id === id ? { ...item, active: !item.active } : 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) => {
|
const handleDragStart = (e: React.DragEvent<HTMLTableRowElement>, item: Kennzahl) => {
|
||||||
|
|
@ -76,6 +120,62 @@ function ConfigPage() {
|
||||||
setDraggedItem(null);
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
minHeight="100vh"
|
minHeight="100vh"
|
||||||
|
|
@ -233,7 +333,7 @@ function ConfigPage() {
|
||||||
border: "1px solid #ddd",
|
border: "1px solid #ddd",
|
||||||
backgroundColor: "#f8f9fa"
|
backgroundColor: "#f8f9fa"
|
||||||
}}>
|
}}>
|
||||||
{kennzahl.format}
|
{getDisplayType(kennzahl.type)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue