diff --git a/project/frontend/src/components/ConfigTable.tsx b/project/frontend/src/components/ConfigTable.tsx index 017d113..74675d7 100644 --- a/project/frontend/src/components/ConfigTable.tsx +++ b/project/frontend/src/components/ConfigTable.tsx @@ -1,322 +1,353 @@ -import { Box, Tooltip, CircularProgress, Typography } from "@mui/material"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; -import { useEffect, useState } from "react"; +import { Box, CircularProgress, Tooltip, Typography } from "@mui/material"; import { useNavigate } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; import type { Kennzahl } from "../types/kpi"; import { getDisplayType } from "../types/kpi"; +import { fetchKennzahlen as fetchK } from "../util/api"; 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); + 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}`); - } + useEffect(() => { + const fetchKennzahlen = async () => { + while (true) { + try { + console.log("Fetching kennzahlen from API..."); + const data = await fetchK(); + 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)); + } + } + }; - 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(); + }, []); - fetchKennzahlen(); - }, []); + const handleToggleActive = async (id: number) => { + const kennzahl = kennzahlen.find((k) => k.id === id); + if (!kennzahl) return; - 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, + }), + }, + ); - 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}`); + } - 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 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 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), + }, + ); - 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}`); + } - 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 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 handleDragStart = (e: React.DragEvent, item: Kennzahl) => { - setDraggedItem(item); - e.dataTransfer.effectAllowed = "move"; - }; + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "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 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 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); - const newKennzahlen = [...kennzahlen]; - const [removed] = newKennzahlen.splice(draggedIndex, 1); - newKennzahlen.splice(targetIndex, 0, removed); + setKennzahlen(newKennzahlen); + setDraggedItem(null); + await updatePositionsInBackend(newKennzahlen); + }; - setKennzahlen(newKennzahlen); - setDraggedItem(null); - await updatePositionsInBackend(newKennzahlen); - }; + const handleDragEnd = () => { + setDraggedItem(null); + }; - const handleDragEnd = () => { - setDraggedItem(null); - }; + const handleRowClick = (kennzahl: Kennzahl, e: React.MouseEvent) => { + if (draggedItem || isUpdatingPositions) { + return; + } - 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; + } - const target = e.target as HTMLElement; - if (target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'checkbox') { - return; - } + if (target.closest(".drag-handle")) { + return; + } - if (target.closest('.drag-handle')) { - return; - } + console.log("Navigating to detail page for KPI:", kennzahl); + console.log("KPI ID:", kennzahl.id); - 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() }, + }); + }; - navigate({ - to: `/config-detail/$kpiId`, - params: { kpiId: kennzahl.id.toString() } - }); - }; + if (loading) { + return ( + + + Lade Kennzahlen-Konfiguration... + + ); + } - 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 + 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)} + +
+
+ ); +} diff --git a/project/frontend/src/components/KennzahlenTable.tsx b/project/frontend/src/components/KennzahlenTable.tsx index 3f2fe75..406798e 100644 --- a/project/frontend/src/components/KennzahlenTable.tsx +++ b/project/frontend/src/components/KennzahlenTable.tsx @@ -1,9 +1,9 @@ +import type { Kennzahl } from "@/types/kpi"; import EditIcon from "@mui/icons-material/Edit"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import SearchIcon from "@mui/icons-material/Search"; import { Box, - IconButton, Link, Paper, Table, @@ -13,6 +13,7 @@ import { TableHead, TableRow, TextField, + Tooltip, } from "@mui/material"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; @@ -20,19 +21,10 @@ import { useState } from "react"; import type { KeyboardEvent } from "react"; import { fetchPutKPI } from "../util/api"; -const SETTINGS = [ - { name: "Rendite", position: 1, active: true, mandatory: true }, - { name: "Ausschüttungsrendite", position: 2, active: true, mandatory: true }, - { name: "Laufzeit", position: 3, active: true, mandatory: true }, - { name: "Länderallokation", position: 4, active: true, mandatory: true }, - { name: "Managmentgebühren", position: 5, active: true, mandatory: true }, - { name: "Risikoprofil", position: 6, active: false, mandatory: true }, - { name: "Irgendwas", position: 7, active: true, mandatory: true }, -]; - interface KennzahlenTableProps { onPageClick?: (page: number) => void; - pdfId: string; // Neue Prop für die PDF-ID + pdfId: string; + settings: Kennzahl[]; data: { [key: string]: { label: string; @@ -44,11 +36,11 @@ interface KennzahlenTableProps { }; } -// React-Komponente export default function KennzahlenTable({ onPageClick, data, pdfId, + settings, }: KennzahlenTableProps) { const [editingIndex, setEditingIndex] = useState(""); const [editValue, setEditValue] = useState(""); @@ -121,6 +113,16 @@ export default function KennzahlenTable({ } }; + const handleNavigateToDetail = (settingName: string) => { + navigate({ + to: "/extractedResult/$pitchBook/$kpi", + params: { + pitchBook: pdfId, + kpi: settingName, + }, + }); + }; + return ( @@ -132,14 +134,15 @@ export default function KennzahlenTable({ Wert - + Seite - {SETTINGS.filter((setting) => setting.active) + {settings + .filter((setting) => setting.active) .sort((a, b) => a.position - b.position) .map((setting) => ({ setting: setting, @@ -147,89 +150,124 @@ export default function KennzahlenTable({ })) .map((row) => { let borderColor = "transparent"; - if ( + const hasMultipleValues = row.extractedValues.length > 1; + const hasNoValue = row.setting.mandatory && (row.extractedValues.length === 0 || - row.extractedValues.at(0)?.entity === "") - ) + row.extractedValues.at(0)?.entity === ""); + + if (hasNoValue) { borderColor = "red"; - else if (row.extractedValues.length > 1) borderColor = "#f6ed48"; + } else if (hasMultipleValues) { + borderColor = "#f6ed48"; + } return ( {row.setting.name} - startEditing( - row.extractedValues.at(0)?.entity || "", - row.setting.name, - ) - } + onClick={() => { + // Only allow inline editing for non-multiple value cells + if (!hasMultipleValues) { + startEditing( + row.extractedValues.at(0)?.entity || "", + row.setting.name, + ); + } else { + // Navigate to detail page for multiple values + handleNavigateToDetail(row.setting.name); + } + }} > - - + Problem +
+ Mehrere Werte für die Kennzahl gefunden. + + } + placement="bottom" + arrow > - {row.setting.mandatory && - row.extractedValues.length === 0 && ( - - )} - {editingIndex === row.setting.name ? ( - setEditValue(e.target.value)} - onKeyDown={(e) => - handleKeyPress(e, row.setting.name) - } - onBlur={() => handleSave(row.setting.name)} - autoFocus - size="small" - fullWidth - variant="standard" - sx={{ margin: "-8px 0" }} - /> - ) : ( - - {row.extractedValues.at(0)?.entity || "—"} - - )} -
- {row.extractedValues.length > 1 && ( - - navigate({ - to: "/extractedResult/$pitchBook/$kpi", - params: { - pitchBook: pdfId, - kpi: row.setting.name, - }, - }) - } + + + + {row.extractedValues.at(0)?.entity || "—"} + + - - )} - {row.extractedValues.length <= 1 && ( +
+ + ) : ( + + + {hasNoValue && ( + + )} + {editingIndex === row.setting.name ? ( + setEditValue(e.target.value)} + onKeyDown={(e) => + handleKeyPress(e, row.setting.name) + } + onBlur={() => handleSave(row.setting.name)} + autoFocus + size="small" + fullWidth + variant="standard" + sx={{ margin: "-8px 0" }} + /> + ) : ( + + {row.extractedValues.at(0)?.entity || "—"} + + )} + - )} - + + )}
- + diff --git a/project/frontend/src/components/pdfViewer.tsx b/project/frontend/src/components/pdfViewer.tsx index a24fa9f..5abc1f4 100644 --- a/project/frontend/src/components/pdfViewer.tsx +++ b/project/frontend/src/components/pdfViewer.tsx @@ -10,11 +10,13 @@ import { socket } from "../socket"; interface PDFViewerProps { pitchBookId: string; currentPage?: number; + onPageChange?: (page: number) => void; } export default function PDFViewer({ pitchBookId, currentPage, + onPageChange, }: PDFViewerProps) { const [numPages, setNumPages] = useState(null); const [pageNumber, setPageNumber] = useState(currentPage || 1); @@ -42,7 +44,7 @@ export default function PDFViewer({ if (currentPage && currentPage !== pageNumber) { setPageNumber(currentPage); } - }, [currentPage, pageNumber]); + }, [currentPage]); useEffect(() => { const handleProgress = (data: { id: number; progress: number }) => { @@ -58,6 +60,11 @@ export default function PDFViewer({ }; }, [pitchBookId]); + const handlePageChange = (newPage: number) => { + setPageNumber(newPage); + onPageChange?.(newPage); + }; + return ( console.error("Ungültige PDF:", error)} > {containerWidth && ( - + )} setPageNumber((p) => p - 1)} + onClick={() => handlePageChange(pageNumber - 1)} > - {pageNumber} / {numPages} - + {pageNumber} / {numPages} + = (numPages || 1)} - onClick={() => setPageNumber((p) => p + 1)} + onClick={() => handlePageChange(pageNumber + 1)} > ); -} +} \ No newline at end of file diff --git a/project/frontend/src/routes/extractedResult.$pitchBook.tsx b/project/frontend/src/routes/extractedResult.$pitchBook.tsx index 3738e09..8888bd8 100644 --- a/project/frontend/src/routes/extractedResult.$pitchBook.tsx +++ b/project/frontend/src/routes/extractedResult.$pitchBook.tsx @@ -5,7 +5,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; import KennzahlenTable from "../components/KennzahlenTable"; import PDFViewer from "../components/pdfViewer"; -import { kpiQueryOptions } from "../util/query"; +import { kpiQueryOptions, settingsQueryOptions } from "../util/query"; // SETTINGS von KennzahlenTable component (mock) const SETTINGS = [ @@ -21,7 +21,10 @@ const SETTINGS = [ export const Route = createFileRoute("/extractedResult/$pitchBook")({ component: ExtractedResultsPage, loader: ({ context: { queryClient }, params: { pitchBook } }) => - queryClient.ensureQueryData(kpiQueryOptions(pitchBook)), + Promise.allSettled([ + queryClient.ensureQueryData(kpiQueryOptions(pitchBook)), + queryClient.ensureQueryData(settingsQueryOptions()), + ]), }); function ExtractedResultsPage() { @@ -39,6 +42,7 @@ function ExtractedResultsPage() { }[status]; const { data: kpi } = useSuspenseQuery(kpiQueryOptions(pitchBook)); + const { data: settings } = useSuspenseQuery(settingsQueryOptions()); const prepareClipboardData = () => { const activeSettings = SETTINGS @@ -111,8 +115,7 @@ function ExtractedResultsPage() { }} /> - Kennzahlen extrahiert aus:
- FONDSNAME: TODO + Extrahierte Kennzahlen
@@ -129,7 +132,8 @@ function ExtractedResultsPage() { elevation={2} sx={{ width: "45%", - height: "100%", + maxHeight: "100%", + height: "fit-content", borderRadius: 2, backgroundColor: "#eeeeee", padding: 2, @@ -137,6 +141,7 @@ function ExtractedResultsPage() { }} > - + - +
+ + + + Gefundene Werte + + + Seite + + + + + {kpiValues.map((item, index) => ( + handleRowClick(index)} + > + + + + {item.entity} + + + + { + e.stopPropagation(); + setCurrentPage(item.page); + }} + sx={{ cursor: 'pointer' }} + > + {item.page} + + + + ))} + + + + { + setSelectedIndex(-1); + }} + > + + { + e.stopPropagation(); + }} + /> + + + + +
+
+ + + + + + + + + + + + + + Achtung + + + + Alle vorgenommenen Änderungen werden verworfen. + + + + + + + + ); -} +} \ No newline at end of file diff --git a/project/frontend/src/util/api.ts b/project/frontend/src/util/api.ts index 8e35d2e..5a8c3c6 100644 --- a/project/frontend/src/util/api.ts +++ b/project/frontend/src/util/api.ts @@ -1,3 +1,5 @@ +import type { Kennzahl } from "@/types/kpi"; + export const fetchKPI = async ( pitchBookId: string, ): Promise<{ @@ -14,7 +16,7 @@ export const fetchKPI = async ( ); const data = await response.json(); - return getKPI(data.kpi); + return data.kpi ? getKPI(data.kpi) : {}; }; export const fetchPutKPI = async ( @@ -95,3 +97,13 @@ export const flattenKPIArray = (kpi: { }) => { return Object.values(kpi).flat(); }; + +export const fetchKennzahlen = async (): Promise => { + 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(); + return data; +}; diff --git a/project/frontend/src/util/query.ts b/project/frontend/src/util/query.ts index 0eaeef8..0821301 100644 --- a/project/frontend/src/util/query.ts +++ b/project/frontend/src/util/query.ts @@ -1,8 +1,14 @@ import { queryOptions } from "@tanstack/react-query"; -import { fetchKPI } from "./api"; +import { fetchKPI, fetchKennzahlen } from "./api"; export const kpiQueryOptions = (pitchBookId: string) => queryOptions({ queryKey: ["pitchBookKPI", pitchBookId], queryFn: () => fetchKPI(pitchBookId), }); + +export const settingsQueryOptions = () => + queryOptions({ + queryKey: ["pitchBookSettings"], + queryFn: () => fetchKennzahlen(), + });