From 51615145f6ccfaee75e6551086fced43fa647ef8 Mon Sep 17 00:00:00 2001 From: s8613 Date: Wed, 4 Jun 2025 21:07:43 +0200 Subject: [PATCH 1/7] Added table to config.tsx --- project/frontend/src/routes/config.tsx | 206 +++++++++++++++++++++++-- 1 file changed, 193 insertions(+), 13 deletions(-) diff --git a/project/frontend/src/routes/config.tsx b/project/frontend/src/routes/config.tsx index 5ddecdb..6ba17e6 100644 --- a/project/frontend/src/routes/config.tsx +++ b/project/frontend/src/routes/config.tsx @@ -1,24 +1,91 @@ import { createFileRoute } from "@tanstack/react-router"; -import { Box, Button, IconButton, Paper, Typography } from "@mui/material"; +import { Box, Button, IconButton, Typography } 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"; export const Route = createFileRoute("/config")({ component: ConfigPage, }); +interface Kennzahl { + id: number; + name: string; + format: string; + 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 }, +]; + function ConfigPage() { const navigate = useNavigate(); + const [kennzahlen, setKennzahlen] = useState(mockKennzahlen); + const [draggedItem, setDraggedItem] = useState(null); + + const handleToggleActive = (id: number) => { + setKennzahlen(prev => + prev.map(item => + item.id === id ? { ...item, active: !item.active } : item + ) + ); + }; + + const handleDragStart = (e: React.DragEvent, item: Kennzahl) => { + setDraggedItem(item); + e.dataTransfer.effectAllowed = "move"; + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + }; + + const handleDrop = (e: React.DragEvent, targetItem: Kennzahl) => { + e.preventDefault(); + if (!draggedItem || draggedItem.id === targetItem.id) return; + + const draggedIndex = kennzahlen.findIndex(item => item.id === draggedItem.id); + const targetIndex = kennzahlen.findIndex(item => item.id === targetItem.id); + + const newKennzahlen = [...kennzahlen]; + const [removed] = newKennzahlen.splice(draggedIndex, 1); + newKennzahlen.splice(targetIndex, 0, removed); + + setKennzahlen(newKennzahlen); + setDraggedItem(null); + }; + + const handleDragEnd = () => { + setDraggedItem(null); + }; return ( - - To-do: Table hierhin - + + + + + + + + + + + {kennzahlen.map((kennzahl) => ( + handleDragStart(e, kennzahl)} + onDragOver={handleDragOver} + onDrop={(e) => handleDrop(e, kennzahl)} + onDragEnd={handleDragEnd} + style={{ + borderBottom: "1px solid #e0e0e0", + cursor: "move", + backgroundColor: draggedItem?.id === kennzahl.id ? "#f0f0f0" : "white", + opacity: draggedItem?.id === kennzahl.id ? 0.5 : 1 + }} + onMouseEnter={(e) => { + if (!draggedItem) { + e.currentTarget.style.backgroundColor = "#f9f9f9"; + } + }} + onMouseLeave={(e) => { + if (!draggedItem) { + e.currentTarget.style.backgroundColor = "white"; + } + }} + > + + + + + + ))} + +
+ + + Aktiv + + Name + + Format +
+ + + handleToggleActive(kennzahl.id)} + style={{ + width: "18px", + height: "18px", + cursor: "pointer", + accentColor: "#383838" + }} + onClick={(e) => e.stopPropagation()} + /> + + {kennzahl.name} + + + {kennzahl.format} + +
+
); } \ No newline at end of file From 59c918cdce3a39f024c1dc39c5618b51ec2e4ee8 Mon Sep 17 00:00:00 2001 From: s8613 Date: Thu, 5 Jun 2025 15:08:44 +0200 Subject: [PATCH 2/7] Frist part of config. Added standard kpi config data. Added integrating from back- and frontend. --- .../controller/kpi_setting_controller.py | 4 + project/backend/coordinator/model/database.py | 2 + .../coordinator/model/kpi_setting_model.py | 9 +- .../backend/coordinator/model/seed_data.py | 184 ++++++++++++++++++ project/backend/spacy-service/Dockerfile | 2 + project/frontend/src/routes/config.tsx | 156 ++++++++++++--- 6 files changed, 328 insertions(+), 29 deletions(-) create mode 100644 project/backend/coordinator/model/seed_data.py 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)} From 521e6918bcfafcdb71fb9fbf67ed7310c345ce55 Mon Sep 17 00:00:00 2001 From: s8613 Date: Thu, 5 Jun 2025 19:56:24 +0200 Subject: [PATCH 3/7] Added detailed page. --- .../controller/kpi_setting_controller.py | 31 ++ .../backend/coordinator/model/seed_data.py | 8 +- .../frontend/src/components/ConfigTable.tsx | 322 ++++++++++++++++ project/frontend/src/routeTree.gen.ts | 38 +- .../src/routes/config-detail.$kpiId.tsx | 345 ++++++++++++++++++ project/frontend/src/routes/config.tsx | 298 +-------------- project/frontend/src/types/kpi.ts | 23 ++ 7 files changed, 764 insertions(+), 301 deletions(-) create mode 100644 project/frontend/src/components/ConfigTable.tsx create mode 100644 project/frontend/src/routes/config-detail.$kpiId.tsx create mode 100644 project/frontend/src/types/kpi.ts diff --git a/project/backend/coordinator/controller/kpi_setting_controller.py b/project/backend/coordinator/controller/kpi_setting_controller.py index 4f48cb2..94fee96 100644 --- a/project/backend/coordinator/controller/kpi_setting_controller.py +++ b/project/backend/coordinator/controller/kpi_setting_controller.py @@ -106,6 +106,12 @@ def update_kpi_setting(id): if "example" in data: kpi_setting.example = data["example"] + if "position" in data: + kpi_setting.position = data["position"] + + if "active" in data: + kpi_setting.active = data["active"] + db.session.commit() return jsonify(kpi_setting.to_dict()), 200 @@ -118,3 +124,28 @@ def delete_kpi_setting(id): db.session.commit() return jsonify({"message": f"KPI Setting {id} deleted successfully"}), 200 + + +@kpi_setting_controller.route("/update-kpi-positions", methods=["PUT"]) +def update_kpi_positions(): + data = request.json + + if not data or not isinstance(data, list): + return jsonify({"error": "Expected an array of update objects"}), 400 + + try: + for update_item in data: + if "id" not in update_item or "position" not in update_item: + return jsonify({"error": "Each item must have 'id' and 'position' fields"}), 400 + + kpi_setting = KPISettingModel.query.get_or_404(update_item["id"]) + kpi_setting.position = update_item["position"] + + db.session.commit() + + updated_kpis = KPISettingModel.query.order_by(KPISettingModel.position).all() + return jsonify([kpi.to_dict() for kpi in updated_kpis]), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Failed to update positions: {str(e)}"}), 500 \ No newline at end of file diff --git a/project/backend/coordinator/model/seed_data.py b/project/backend/coordinator/model/seed_data.py index 30b50a3..e13145b 100644 --- a/project/backend/coordinator/model/seed_data.py +++ b/project/backend/coordinator/model/seed_data.py @@ -63,7 +63,7 @@ def seed_default_kpi_settings(): "mandatory": False, "type": KPISettingType.BOOLEAN, "translation": "Article", - "example": "Ja", + "example": "Artikel 8", "position": 6, "active": True }, @@ -113,7 +113,7 @@ def seed_default_kpi_settings(): "mandatory": True, "type": KPISettingType.STRING, "translation": "Duration", - "example": "7 Jahre", + "example": "7 Jahre, 10, Evergreen", "position": 11, "active": True }, @@ -143,7 +143,7 @@ def seed_default_kpi_settings(): "mandatory": False, "type": KPISettingType.ARRAY, "translation": "Sector Allocation", - "example": "Büroimmobilien, Einzelhandel, Logistik", + "example": "Büro, Wohnen, Logistik, Studentenwohnen", "position": 14, "active": True }, @@ -153,7 +153,7 @@ def seed_default_kpi_settings(): "mandatory": False, "type": KPISettingType.ARRAY, "translation": "Country Allocation", - "example": "Deutschland, Österreich, Schweiz", + "example": "Deutschland,Frankreich, Österreich, Schweiz", "position": 15, "active": True } diff --git a/project/frontend/src/components/ConfigTable.tsx b/project/frontend/src/components/ConfigTable.tsx new file mode 100644 index 0000000..017d113 --- /dev/null +++ b/project/frontend/src/components/ConfigTable.tsx @@ -0,0 +1,322 @@ +import { Box, Tooltip, CircularProgress, Typography } from "@mui/material"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import { useEffect, useState } from "react"; +import { useNavigate } from "@tanstack/react-router"; +import type { Kennzahl } from "../types/kpi"; +import { getDisplayType } from "../types/kpi"; + +export function ConfigTable() { + const navigate = useNavigate(); + const [kennzahlen, setKennzahlen] = useState([]); + const [draggedItem, setDraggedItem] = useState(null); + const [isUpdatingPositions, setIsUpdatingPositions] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchKennzahlen = async () => { + while (true) { + try { + console.log('Fetching kennzahlen from API...'); + 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(); + console.log('Fetched kennzahlen:', data); + const sortedData = data.sort((a: Kennzahl, b: Kennzahl) => a.position - b.position); + setKennzahlen(sortedData); + setLoading(false); + break; + } catch (err) { + console.error('Error fetching kennzahlen:', err); + await new Promise(resolve => setTimeout(resolve, 2000)); + } + } + }; + + 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({ + active: !kennzahl.active + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const updatedKennzahl = await response.json(); + setKennzahlen(prev => + prev.map(item => + item.id === id ? updatedKennzahl : item + ) + ); + } catch (err) { + console.error('Error toggling active status:', err); + setKennzahlen(prev => + prev.map(item => + item.id === id ? kennzahl : item + ) + ); + } + }; + + const updatePositionsInBackend = async (reorderedKennzahlen: Kennzahl[]) => { + setIsUpdatingPositions(true); + try { + const positionUpdates = reorderedKennzahlen.map((kennzahl, index) => ({ + id: kennzahl.id, + position: index + 1 + })); + + const response = await fetch(`http://localhost:5050/api/kpi_setting/update-kpi-positions`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(positionUpdates), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const updatedKennzahlen = await response.json(); + setKennzahlen(updatedKennzahlen); + } catch (err) { + console.error('Error updating positions:', err); + window.location.reload(); + } finally { + setIsUpdatingPositions(false); + } + }; + + const handleDragStart = (e: React.DragEvent, item: Kennzahl) => { + setDraggedItem(item); + e.dataTransfer.effectAllowed = "move"; + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + }; + + const handleDrop = async (e: React.DragEvent, targetItem: Kennzahl) => { + e.preventDefault(); + if (!draggedItem || draggedItem.id === targetItem.id) return; + + const draggedIndex = kennzahlen.findIndex(item => item.id === draggedItem.id); + const targetIndex = kennzahlen.findIndex(item => item.id === targetItem.id); + + const newKennzahlen = [...kennzahlen]; + const [removed] = newKennzahlen.splice(draggedIndex, 1); + newKennzahlen.splice(targetIndex, 0, removed); + + setKennzahlen(newKennzahlen); + setDraggedItem(null); + await updatePositionsInBackend(newKennzahlen); + }; + + const handleDragEnd = () => { + setDraggedItem(null); + }; + + const handleRowClick = (kennzahl: Kennzahl, e: React.MouseEvent) => { + if (draggedItem || isUpdatingPositions) { + return; + } + + const target = e.target as HTMLElement; + if (target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'checkbox') { + return; + } + + if (target.closest('.drag-handle')) { + return; + } + + console.log('Navigating to detail page for KPI:', kennzahl); + console.log('KPI ID:', kennzahl.id); + + navigate({ + to: `/config-detail/$kpiId`, + params: { kpiId: kennzahl.id.toString() } + }); + }; + + if (loading) { + return ( + + + Lade Kennzahlen-Konfiguration... + + ); + } + + return ( + + + + + + + + + + + + {kennzahlen.map((kennzahl) => ( + handleDragStart(e, kennzahl)} + onDragOver={handleDragOver} + onDrop={(e) => handleDrop(e, kennzahl)} + onDragEnd={handleDragEnd} + onClick={(e) => handleRowClick(kennzahl, e)} + style={{ + borderBottom: "1px solid #e0e0e0", + cursor: isUpdatingPositions ? "default" : "pointer", + backgroundColor: draggedItem?.id === kennzahl.id ? "#f0f0f0" : "white", + opacity: draggedItem?.id === kennzahl.id ? 0.5 : 1 + }} + onMouseEnter={(e) => { + if (!draggedItem && !isUpdatingPositions) { + e.currentTarget.style.backgroundColor = "#f9f9f9"; + } + }} + onMouseLeave={(e) => { + if (!draggedItem && !isUpdatingPositions) { + e.currentTarget.style.backgroundColor = "white"; + } + }} + > + + + + + + ))} + +
+ + Aktiv + + Name + + Format +
+
+ + Neuanordnung der Kennzahlen
+ Hier können Sie die Kennzahlen nach Belieben per Drag and Drop neu anordnen. + + } + placement="left" + arrow + > + +
+
+
+ handleToggleActive(kennzahl.id)} + disabled={isUpdatingPositions} + style={{ + width: "18px", + height: "18px", + cursor: isUpdatingPositions ? "default" : "pointer", + accentColor: "#383838" + }} + onClick={(e) => e.stopPropagation()} + /> + + + {kennzahl.name} + + + + {getDisplayType(kennzahl.type)} + +
+
+ ); +} \ No newline at end of file diff --git a/project/frontend/src/routeTree.gen.ts b/project/frontend/src/routeTree.gen.ts index b3c2de7..021e778 100644 --- a/project/frontend/src/routeTree.gen.ts +++ b/project/frontend/src/routeTree.gen.ts @@ -14,6 +14,7 @@ import { Route as rootRoute } from './routes/__root' import { Route as ConfigImport } from './routes/config' import { Route as IndexImport } from './routes/index' import { Route as ExtractedResultPitchBookImport } from './routes/extractedResult.$pitchBook' +import { Route as ConfigDetailKpiIdImport } from './routes/config-detail.$kpiId' // Create/Update Routes @@ -35,6 +36,12 @@ const ExtractedResultPitchBookRoute = ExtractedResultPitchBookImport.update({ getParentRoute: () => rootRoute, } as any) +const ConfigDetailKpiIdRoute = ConfigDetailKpiIdImport.update({ + id: '/config-detail/$kpiId', + path: '/config-detail/$kpiId', + getParentRoute: () => rootRoute, +} as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -53,6 +60,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ConfigImport parentRoute: typeof rootRoute } + '/config-detail/$kpiId': { + id: '/config-detail/$kpiId' + path: '/config-detail/$kpiId' + fullPath: '/config-detail/$kpiId' + preLoaderRoute: typeof ConfigDetailKpiIdImport + parentRoute: typeof rootRoute + } '/extractedResult/$pitchBook': { id: '/extractedResult/$pitchBook' path: '/extractedResult/$pitchBook' @@ -68,12 +82,14 @@ declare module '@tanstack/react-router' { export interface FileRoutesByFullPath { '/': typeof IndexRoute '/config': typeof ConfigRoute + '/config-detail/$kpiId': typeof ConfigDetailKpiIdRoute '/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/config': typeof ConfigRoute + '/config-detail/$kpiId': typeof ConfigDetailKpiIdRoute '/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute } @@ -81,27 +97,39 @@ export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute '/config': typeof ConfigRoute + '/config-detail/$kpiId': typeof ConfigDetailKpiIdRoute '/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/config' | '/extractedResult/$pitchBook' + fullPaths: + | '/' + | '/config' + | '/config-detail/$kpiId' + | '/extractedResult/$pitchBook' fileRoutesByTo: FileRoutesByTo - to: '/' | '/config' | '/extractedResult/$pitchBook' - id: '__root__' | '/' | '/config' | '/extractedResult/$pitchBook' + to: '/' | '/config' | '/config-detail/$kpiId' | '/extractedResult/$pitchBook' + id: + | '__root__' + | '/' + | '/config' + | '/config-detail/$kpiId' + | '/extractedResult/$pitchBook' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute ConfigRoute: typeof ConfigRoute + ConfigDetailKpiIdRoute: typeof ConfigDetailKpiIdRoute ExtractedResultPitchBookRoute: typeof ExtractedResultPitchBookRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, ConfigRoute: ConfigRoute, + ConfigDetailKpiIdRoute: ConfigDetailKpiIdRoute, ExtractedResultPitchBookRoute: ExtractedResultPitchBookRoute, } @@ -117,6 +145,7 @@ export const routeTree = rootRoute "children": [ "/", "/config", + "/config-detail/$kpiId", "/extractedResult/$pitchBook" ] }, @@ -126,6 +155,9 @@ export const routeTree = rootRoute "/config": { "filePath": "config.tsx" }, + "/config-detail/$kpiId": { + "filePath": "config-detail.$kpiId.tsx" + }, "/extractedResult/$pitchBook": { "filePath": "extractedResult.$pitchBook.tsx" } diff --git a/project/frontend/src/routes/config-detail.$kpiId.tsx b/project/frontend/src/routes/config-detail.$kpiId.tsx new file mode 100644 index 0000000..e4cabc8 --- /dev/null +++ b/project/frontend/src/routes/config-detail.$kpiId.tsx @@ -0,0 +1,345 @@ +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { + Box, + Typography, + IconButton, + Button, + Paper, + TextField, + FormControlLabel, + Checkbox, + Select, + MenuItem, + FormControl, + InputLabel, + Divider, + CircularProgress +} from "@mui/material"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import { useEffect, useState } from "react"; +import type { Kennzahl } from "../types/kpi"; +import { typeDisplayMapping } from "../types/kpi"; + +export const Route = createFileRoute("/config-detail/$kpiId")({ + component: KPIDetailPage, +}); + +function KPIDetailPage() { + const { kpiId } = Route.useParams(); + const navigate = useNavigate(); + const [kennzahl, setKennzahl] = useState(null); + const [isEditing, setIsEditing] = useState(false); + const [editedKennzahl, setEditedKennzahl] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchKennzahl = async () => { + try { + setLoading(true); + const response = await fetch(`http://localhost:5050/api/kpi_setting/${kpiId}`); + if (!response.ok) { + if (response.status === 404) { + setError('KPI not found'); + return; + } + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + setKennzahl(data); + setEditedKennzahl(data); + setError(null); + } catch (err) { + console.error('Error fetching KPI:', err); + setError('Error loading KPI'); + } finally { + setLoading(false); + } + }; + + fetchKennzahl(); + }, [kpiId]); + + const handleSave = async () => { + if (!editedKennzahl) return; + + try { + const response = await fetch(`http://localhost:5050/api/kpi_setting/${kpiId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(editedKennzahl), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const updatedKennzahl = await response.json(); + setKennzahl(updatedKennzahl); + setEditedKennzahl(updatedKennzahl); + setIsEditing(false); + } catch (err) { + console.error('Error saving KPI:', err); + } + }; + + const handleCancel = () => { + setEditedKennzahl(kennzahl); + setIsEditing(false); + }; + + if (loading) { + return ( + + + Lade KPI Details... + + ); + } + + if (error || !kennzahl) { + return ( + + + {error || 'KPI nicht gefunden'} + + + + ); + } + + const currentKennzahl = isEditing ? editedKennzahl! : kennzahl; + + return ( + + + + navigate({ to: "/config" })}> + + + + Detailansicht + + + + + + + + Kennzahl + + + {isEditing ? ( + setEditedKennzahl(prev => prev ? {...prev, name: e.target.value} : null)} + sx={{ mb: 2 }} + /> + ) : ( + + {currentKennzahl.name} + + )} + + + + + + Beschreibung + + + {isEditing ? ( + setEditedKennzahl(prev => prev ? {...prev, description: e.target.value} : null)} + helperText="Beschreibung der Kennzahl" + /> + ) : ( + + {currentKennzahl.description || "Zurzeit ist die Beschreibung der Kennzahl leer. Klicken Sie auf den Bearbeiten-Button, um die Beschreibung zu ergänzen."} + + )} + + + isEditing && setEditedKennzahl(prev => prev ? {...prev, mandatory: e.target.checked} : null)} + disabled={!isEditing} + sx={{ color: '#383838' }} + /> + } + label="Erforderlich" + /> + + Die Kennzahl erlaubt keine leeren Werte + + + + + + + + + Format: {typeDisplayMapping[currentKennzahl.type] || currentKennzahl.type} + + + {isEditing ? ( + + Typ + + + ) : null} + + + + + + Synonyme & Übersetzungen + + + {isEditing ? ( + setEditedKennzahl(prev => prev ? {...prev, translation: e.target.value} : null)} + helperText="z.B. Englische Übersetzung der Kennzahl" + /> + ) : ( + + {currentKennzahl.translation || "Zurzeit gibt es keine Einträge für Synonyme und Übersetzungen der Kennzahl. Klicken Sie auf den Bearbeiten-Button, um die Liste zu ergänzen."} + + )} + + + + + + Beispiele von Kennzahl + + + {isEditing ? ( + setEditedKennzahl(prev => prev ? {...prev, example: e.target.value} : null)} + helperText="Beispielwerte für diese Kennzahl" + /> + ) : ( + + {currentKennzahl.example || "Zurzeit gibt es keine Beispiele der Kennzahl. Klicken Sie auf den Bearbeiten-Button, um die Liste zu ergänzen."} + + )} + + {isEditing && ( + + + + + )} + + + ); +} \ No newline at end of file diff --git a/project/frontend/src/routes/config.tsx b/project/frontend/src/routes/config.tsx index 29eea58..a30f3ab 100644 --- a/project/frontend/src/routes/config.tsx +++ b/project/frontend/src/routes/config.tsx @@ -1,180 +1,16 @@ import { createFileRoute } from "@tanstack/react-router"; -import { Box, Button, IconButton, Typography, CircularProgress } from "@mui/material"; +import { Box, Button, IconButton, Typography } from "@mui/material"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; import { useNavigate } from "@tanstack/react-router"; -import { useEffect, useState } from "react"; +import { ConfigTable } from "../components/ConfigTable"; export const Route = createFileRoute("/config")({ component: ConfigPage, }); -interface Kennzahl { - id: number; - name: string; - description: string; - mandatory: boolean; - type: string; - translation: string; - example: string; - position: number; - active: boolean; -} - -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([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [draggedItem, setDraggedItem] = useState(null); - - 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) => { - setDraggedItem(item); - e.dataTransfer.effectAllowed = "move"; - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - }; - - const handleDrop = (e: React.DragEvent, targetItem: Kennzahl) => { - e.preventDefault(); - if (!draggedItem || draggedItem.id === targetItem.id) return; - - const draggedIndex = kennzahlen.findIndex(item => item.id === draggedItem.id); - const targetIndex = kennzahlen.findIndex(item => item.id === targetItem.id); - - const newKennzahlen = [...kennzahlen]; - const [removed] = newKennzahlen.splice(draggedIndex, 1); - newKennzahlen.splice(targetIndex, 0, removed); - - setKennzahlen(newKennzahlen); - setDraggedItem(null); - }; - - const handleDragEnd = () => { - setDraggedItem(null); - }; - - if (loading) { - return ( - - - Lade Kennzahlen... - - ); - } - - if (error) { - return ( - - - Fehler beim Laden der Daten - - - {error} - - - - - - - ); - } return ( - - - - - - - - - - - - - {kennzahlen.map((kennzahl) => ( - handleDragStart(e, kennzahl)} - onDragOver={handleDragOver} - onDrop={(e) => handleDrop(e, kennzahl)} - onDragEnd={handleDragEnd} - style={{ - borderBottom: "1px solid #e0e0e0", - cursor: "move", - backgroundColor: draggedItem?.id === kennzahl.id ? "#f0f0f0" : "white", - opacity: draggedItem?.id === kennzahl.id ? 0.5 : 1 - }} - onMouseEnter={(e) => { - if (!draggedItem) { - e.currentTarget.style.backgroundColor = "#f9f9f9"; - } - }} - onMouseLeave={(e) => { - if (!draggedItem) { - e.currentTarget.style.backgroundColor = "white"; - } - }} - > - - - - - - ))} - -
- - - Aktiv - - Name - - Format -
- - - handleToggleActive(kennzahl.id)} - style={{ - width: "18px", - height: "18px", - cursor: "pointer", - accentColor: "#383838" - }} - onClick={(e) => e.stopPropagation()} - /> - - {kennzahl.name} - - - {getDisplayType(kennzahl.type)} - -
+ +
); diff --git a/project/frontend/src/types/kpi.ts b/project/frontend/src/types/kpi.ts new file mode 100644 index 0000000..c8d66da --- /dev/null +++ b/project/frontend/src/types/kpi.ts @@ -0,0 +1,23 @@ +export interface Kennzahl { + id: number; + name: string; + description: string; + mandatory: boolean; + type: string; + translation: string; + example: string; + position: number; + active: boolean; +} + +export const typeDisplayMapping: Record = { + "string": "Text", + "date": "Datum", + "boolean": "Ja/Nein", + "number": "Zahl", + "array": "Liste (mehrfach)" +}; + +export const getDisplayType = (backendType: string): string => { + return typeDisplayMapping[backendType] || backendType; +}; \ No newline at end of file From 509413f9948acd43248e3f6f29a23c29080d265c Mon Sep 17 00:00:00 2001 From: s8613 Date: Thu, 5 Jun 2025 21:45:13 +0200 Subject: [PATCH 4/7] Added Config add page. --- project/frontend/src/components/KPIForm.tsx | 252 +++++++++++++ project/frontend/src/routeTree.gen.ts | 32 +- project/frontend/src/routes/config-add.tsx | 87 +++++ .../src/routes/config-detail.$kpiId.tsx | 330 +++++++----------- project/frontend/src/routes/config.tsx | 6 +- 5 files changed, 502 insertions(+), 205 deletions(-) create mode 100644 project/frontend/src/components/KPIForm.tsx create mode 100644 project/frontend/src/routes/config-add.tsx diff --git a/project/frontend/src/components/KPIForm.tsx b/project/frontend/src/components/KPIForm.tsx new file mode 100644 index 0000000..e493495 --- /dev/null +++ b/project/frontend/src/components/KPIForm.tsx @@ -0,0 +1,252 @@ +import { Box, Typography, Button, Paper, TextField, FormControlLabel, + Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress } from "@mui/material"; +import { useState, useEffect } from "react"; +import type { Kennzahl } from "../types/kpi"; +import { typeDisplayMapping } from "../types/kpi"; + +interface KPIFormProps { + mode: 'add' | 'edit'; + initialData?: Kennzahl | null; + onSave: (data: Partial) => Promise; + onCancel: () => void; + loading?: boolean; +} + +const emptyKPI: Partial = { + name: '', + description: '', + mandatory: false, + type: 'string', + translation: '', + example: '', + active: true +}; + +export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }: KPIFormProps) { + const [formData, setFormData] = useState>(emptyKPI); + const [isSaving, setIsSaving] = useState(false); + + useEffect(() => { + if (mode === 'edit' && initialData) { + setFormData(initialData); + } else { + setFormData(emptyKPI); + } + }, [mode, initialData]); + + const handleSave = async () => { + if (!formData.name?.trim()) { + alert('Name ist erforderlich'); + return; + } + + setIsSaving(true); + try { + await onSave(formData); + } catch (error) { + console.error('Error saving KPI:', error); + } finally { + setIsSaving(false); + } + }; + + const handleCancel = () => { + if (mode === 'edit' && initialData) { + setFormData(initialData); + } else { + setFormData(emptyKPI); + } + onCancel(); + }; + + const updateField = (field: keyof Kennzahl, value: any) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + if (loading) { + return ( + + + + {mode === 'edit' ? 'Lade KPI Details...' : 'Laden...'} + + + ); + } + + return ( + + + + Kennzahl + + updateField('name', e.target.value)} + sx={{ mb: 2 }} + required + error={!formData.name?.trim()} + helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''} + /> + + + + + + + Beschreibung + + updateField('description', e.target.value)} + helperText="Beschreibung der Kennzahl" + /> + + + updateField('mandatory', e.target.checked)} + sx={{ color: '#383838' }} + /> + } + label="Erforderlich" + /> + + Die Kennzahl erlaubt keine leeren Werte + + + + + + + + + Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type} + + + Typ + + + + + + + + + Synonyme & Übersetzungen + + updateField('translation', e.target.value)} + helperText="z.B. Englische Übersetzung der Kennzahl" + /> + + + + + + + Beispiele von Kennzahl + + updateField('example', e.target.value)} + helperText="Beispielwerte für diese Kennzahl" + /> + + + {mode === 'add' && ( + <> + + + updateField('active', e.target.checked)} + sx={{ color: '#383838' }} + /> + } + label="Aktiv" + /> + + Die Kennzahl ist aktiv und wird angezeigt + + + + )} + + + + + + + ); +} \ No newline at end of file diff --git a/project/frontend/src/routeTree.gen.ts b/project/frontend/src/routeTree.gen.ts index 021e778..b426ec2 100644 --- a/project/frontend/src/routeTree.gen.ts +++ b/project/frontend/src/routeTree.gen.ts @@ -11,6 +11,7 @@ // Import Routes import { Route as rootRoute } from './routes/__root' +import { Route as ConfigAddImport } from './routes/config-add' import { Route as ConfigImport } from './routes/config' import { Route as IndexImport } from './routes/index' import { Route as ExtractedResultPitchBookImport } from './routes/extractedResult.$pitchBook' @@ -18,6 +19,12 @@ import { Route as ConfigDetailKpiIdImport } from './routes/config-detail.$kpiId' // Create/Update Routes +const ConfigAddRoute = ConfigAddImport.update({ + id: '/config-add', + path: '/config-add', + getParentRoute: () => rootRoute, +} as any) + const ConfigRoute = ConfigImport.update({ id: '/config', path: '/config', @@ -60,6 +67,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ConfigImport parentRoute: typeof rootRoute } + '/config-add': { + id: '/config-add' + path: '/config-add' + fullPath: '/config-add' + preLoaderRoute: typeof ConfigAddImport + parentRoute: typeof rootRoute + } '/config-detail/$kpiId': { id: '/config-detail/$kpiId' path: '/config-detail/$kpiId' @@ -82,6 +96,7 @@ declare module '@tanstack/react-router' { export interface FileRoutesByFullPath { '/': typeof IndexRoute '/config': typeof ConfigRoute + '/config-add': typeof ConfigAddRoute '/config-detail/$kpiId': typeof ConfigDetailKpiIdRoute '/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute } @@ -89,6 +104,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/config': typeof ConfigRoute + '/config-add': typeof ConfigAddRoute '/config-detail/$kpiId': typeof ConfigDetailKpiIdRoute '/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute } @@ -97,6 +113,7 @@ export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute '/config': typeof ConfigRoute + '/config-add': typeof ConfigAddRoute '/config-detail/$kpiId': typeof ConfigDetailKpiIdRoute '/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute } @@ -106,14 +123,21 @@ export interface FileRouteTypes { fullPaths: | '/' | '/config' + | '/config-add' | '/config-detail/$kpiId' | '/extractedResult/$pitchBook' fileRoutesByTo: FileRoutesByTo - to: '/' | '/config' | '/config-detail/$kpiId' | '/extractedResult/$pitchBook' + to: + | '/' + | '/config' + | '/config-add' + | '/config-detail/$kpiId' + | '/extractedResult/$pitchBook' id: | '__root__' | '/' | '/config' + | '/config-add' | '/config-detail/$kpiId' | '/extractedResult/$pitchBook' fileRoutesById: FileRoutesById @@ -122,6 +146,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute ConfigRoute: typeof ConfigRoute + ConfigAddRoute: typeof ConfigAddRoute ConfigDetailKpiIdRoute: typeof ConfigDetailKpiIdRoute ExtractedResultPitchBookRoute: typeof ExtractedResultPitchBookRoute } @@ -129,6 +154,7 @@ export interface RootRouteChildren { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, ConfigRoute: ConfigRoute, + ConfigAddRoute: ConfigAddRoute, ConfigDetailKpiIdRoute: ConfigDetailKpiIdRoute, ExtractedResultPitchBookRoute: ExtractedResultPitchBookRoute, } @@ -145,6 +171,7 @@ export const routeTree = rootRoute "children": [ "/", "/config", + "/config-add", "/config-detail/$kpiId", "/extractedResult/$pitchBook" ] @@ -155,6 +182,9 @@ export const routeTree = rootRoute "/config": { "filePath": "config.tsx" }, + "/config-add": { + "filePath": "config-add.tsx" + }, "/config-detail/$kpiId": { "filePath": "config-detail.$kpiId.tsx" }, diff --git a/project/frontend/src/routes/config-add.tsx b/project/frontend/src/routes/config-add.tsx new file mode 100644 index 0000000..b90d7b1 --- /dev/null +++ b/project/frontend/src/routes/config-add.tsx @@ -0,0 +1,87 @@ +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { Box, Typography, IconButton } from "@mui/material"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import { KPIForm } from "../components/KPIForm"; +import type { Kennzahl } from "../types/kpi"; + +export const Route = createFileRoute("/config-add")({ + component: ConfigAddPage, +}); + +function ConfigAddPage() { + const navigate = useNavigate(); + + const handleSave = async (formData: Partial) => { + try { + const existingKPIsResponse = await fetch('http://localhost:5050/api/kpi_setting/'); + const existingKPIs = await existingKPIsResponse.json(); + const maxPosition = existingKPIs.length > 0 + ? Math.max(...existingKPIs.map((kpi: Kennzahl) => kpi.position)) + : 0; + + const kpiData = { + ...formData, + position: maxPosition + 1, + active: formData.active !== false + }; + + const response = await fetch('http://localhost:5050/api/kpi_setting/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(kpiData), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + navigate({ to: "/config" }); + } catch (error) { + console.error('Error creating KPI:', error); + throw error; + } + }; + + const handleCancel = () => { + navigate({ to: "/config" }); + }; + + return ( + + + + navigate({ to: "/config" })}> + + + + Neue Kennzahl hinzufügen + + + + + + + ); +} \ No newline at end of file diff --git a/project/frontend/src/routes/config-detail.$kpiId.tsx b/project/frontend/src/routes/config-detail.$kpiId.tsx index e4cabc8..16ddce9 100644 --- a/project/frontend/src/routes/config-detail.$kpiId.tsx +++ b/project/frontend/src/routes/config-detail.$kpiId.tsx @@ -1,23 +1,10 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import { - Box, - Typography, - IconButton, - Button, - Paper, - TextField, - FormControlLabel, - Checkbox, - Select, - MenuItem, - FormControl, - InputLabel, - Divider, - CircularProgress +import { Box, Typography, IconButton, Button, CircularProgress, Paper, Divider } from "@mui/material"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import { useEffect, useState } from "react"; import type { Kennzahl } from "../types/kpi"; +import { KPIForm } from "../components/KPIForm"; import { typeDisplayMapping } from "../types/kpi"; export const Route = createFileRoute("/config-detail/$kpiId")({ @@ -29,7 +16,6 @@ function KPIDetailPage() { const navigate = useNavigate(); const [kennzahl, setKennzahl] = useState(null); const [isEditing, setIsEditing] = useState(false); - const [editedKennzahl, setEditedKennzahl] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -47,7 +33,6 @@ function KPIDetailPage() { } const data = await response.json(); setKennzahl(data); - setEditedKennzahl(data); setError(null); } catch (err) { console.error('Error fetching KPI:', err); @@ -60,16 +45,14 @@ function KPIDetailPage() { fetchKennzahl(); }, [kpiId]); - const handleSave = async () => { - if (!editedKennzahl) return; - + const handleSave = async (formData: Partial) => { try { const response = await fetch(`http://localhost:5050/api/kpi_setting/${kpiId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(editedKennzahl), + body: JSON.stringify(formData), }); if (!response.ok) { @@ -78,15 +61,14 @@ function KPIDetailPage() { const updatedKennzahl = await response.json(); setKennzahl(updatedKennzahl); - setEditedKennzahl(updatedKennzahl); setIsEditing(false); - } catch (err) { - console.error('Error saving KPI:', err); + } catch (error) { + console.error('Error saving KPI:', error); + throw error; } }; const handleCancel = () => { - setEditedKennzahl(kennzahl); setIsEditing(false); }; @@ -118,7 +100,7 @@ function KPIDetailPage() { alignItems="center" flexDirection="column" > - + {error || 'KPI nicht gefunden'} +
+ + + + Kennzahl + + + {kennzahl.name} + + + + + + + + Beschreibung + + + {kennzahl.description || "Zurzeit ist die Beschreibung der Kennzahl leer. Klicken Sie auf den Bearbeiten-Button, um die Beschreibung zu ergänzen."} + + + + + Erforderlich: {kennzahl.mandatory ? 'Ja' : 'Nein'} + + + + + + + + + Format + + + {typeDisplayMapping[kennzahl.type] || kennzahl.type} + + + + + + + + Synonyme & Übersetzungen + + + {kennzahl.translation || "Zurzeit gibt es keine Einträge für Synonyme und Übersetzungen der Kennzahl. Klicken Sie auf den Bearbeiten-Button, um die Liste zu ergänzen."} + + + + + + + + Beispiele von Kennzahl + + + {kennzahl.example || "Zurzeit gibt es keine Beispiele der Kennzahl. Klicken Sie auf den Bearbeiten-Button, um die Liste zu ergänzen."} + + + + + ); + } return ( - Detailansicht + Kennzahl bearbeiten - - - - - Kennzahl - - {isEditing ? ( - setEditedKennzahl(prev => prev ? {...prev, name: e.target.value} : null)} - sx={{ mb: 2 }} - /> - ) : ( - - {currentKennzahl.name} - - )} - - - - - - Beschreibung - - - {isEditing ? ( - setEditedKennzahl(prev => prev ? {...prev, description: e.target.value} : null)} - helperText="Beschreibung der Kennzahl" - /> - ) : ( - - {currentKennzahl.description || "Zurzeit ist die Beschreibung der Kennzahl leer. Klicken Sie auf den Bearbeiten-Button, um die Beschreibung zu ergänzen."} - - )} - - - isEditing && setEditedKennzahl(prev => prev ? {...prev, mandatory: e.target.checked} : null)} - disabled={!isEditing} - sx={{ color: '#383838' }} - /> - } - label="Erforderlich" - /> - - Die Kennzahl erlaubt keine leeren Werte - - - - - - - - - Format: {typeDisplayMapping[currentKennzahl.type] || currentKennzahl.type} - - - {isEditing ? ( - - Typ - - - ) : null} - - - - - - Synonyme & Übersetzungen - - - {isEditing ? ( - setEditedKennzahl(prev => prev ? {...prev, translation: e.target.value} : null)} - helperText="z.B. Englische Übersetzung der Kennzahl" - /> - ) : ( - - {currentKennzahl.translation || "Zurzeit gibt es keine Einträge für Synonyme und Übersetzungen der Kennzahl. Klicken Sie auf den Bearbeiten-Button, um die Liste zu ergänzen."} - - )} - - - - - - Beispiele von Kennzahl - - - {isEditing ? ( - setEditedKennzahl(prev => prev ? {...prev, example: e.target.value} : null)} - helperText="Beispielwerte für diese Kennzahl" - /> - ) : ( - - {currentKennzahl.example || "Zurzeit gibt es keine Beispiele der Kennzahl. Klicken Sie auf den Bearbeiten-Button, um die Liste zu ergänzen."} - - )} - - {isEditing && ( - - - - - )} - + ); } \ No newline at end of file diff --git a/project/frontend/src/routes/config.tsx b/project/frontend/src/routes/config.tsx index a30f3ab..eddf16e 100644 --- a/project/frontend/src/routes/config.tsx +++ b/project/frontend/src/routes/config.tsx @@ -8,10 +8,13 @@ export const Route = createFileRoute("/config")({ component: ConfigPage, }); - function ConfigPage() { const navigate = useNavigate(); + const handleAddNewKPI = () => { + navigate({ to: "/config-add" }); + }; + return (