diff --git a/project/backend/coordinator/controller/socketIO.py b/project/backend/coordinator/controller/socketIO.py index 81ee5d4..2a82575 100644 --- a/project/backend/coordinator/controller/socketIO.py +++ b/project/backend/coordinator/controller/socketIO.py @@ -1,4 +1,3 @@ from flask_socketio import SocketIO -socketio = SocketIO(cors_allowed_origins=["http://localhost:8080", "http://localhost:3000"], - transports=['polling', 'websocket'] ) +socketio = SocketIO(cors_allowed_origins="*") diff --git a/project/frontend/src/components/KennzahlenTable.tsx b/project/frontend/src/components/KennzahlenTable.tsx index ce87bad..3f2fe75 100644 --- a/project/frontend/src/components/KennzahlenTable.tsx +++ b/project/frontend/src/components/KennzahlenTable.tsx @@ -1,211 +1,265 @@ +import EditIcon from "@mui/icons-material/Edit"; +import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; +import SearchIcon from "@mui/icons-material/Search"; import { - Table, TableBody, TableCell, TableContainer, - TableHead, TableRow, Paper, Box, - TextField, Link - } from '@mui/material'; - import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; - import SearchIcon from '@mui/icons-material/Search'; - import EditIcon from '@mui/icons-material/Edit'; - import { useState, useEffect } from 'react'; - import type { KeyboardEvent } from 'react'; - - const API_BASE_URL = 'http://localhost:5050'; // Korrigierter Port für den Coordinator-Service - - interface Kennzahl { - pdf_id: string; - label: string; - value: string; - page: number; - status: 'ok' | 'error' | 'warning'; - } - - interface KennzahlenTableProps { - onPageClick?: (page: number) => void; - pdfId?: string; // Neue Prop für die PDF-ID - } - - // React-Komponente - export default function KennzahlenTable({ onPageClick, pdfId = 'example' }: KennzahlenTableProps) { - const [rows, setRows] = useState([]); - const [editingIndex, setEditingIndex] = useState(null); - const [editValue, setEditValue] = useState(''); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - // Initialisiere Beispieldaten - const initializeData = async () => { - try { - const response = await fetch(`${API_BASE_URL}/api/kennzahlen/init`, { - method: 'POST' - }); - if (!response.ok) { - throw new Error('Fehler beim Initialisieren der Daten'); - } - // Lade die Daten nach der Initialisierung - await fetchKennzahlen(); - } catch (err) { - setError('Fehler beim Initialisieren der Daten'); - console.error('Fehler:', err); - } - }; - - // Lade Kennzahlen vom Backend - const fetchKennzahlen = async () => { - try { - const response = await fetch(`${API_BASE_URL}/api/kennzahlen?pdf_id=${pdfId}`); - if (!response.ok) { - throw new Error('Fehler beim Laden der Kennzahlen'); - } - const data = await response.json(); - if (data.length === 0) { - // Wenn keine Daten vorhanden sind, initialisiere Beispieldaten - await initializeData(); - } else { - setRows(data); - setError(null); - } - } catch (err) { - setError('Fehler beim Laden der Daten'); - console.error('Fehler:', err); - } finally { - setIsLoading(false); - } - }; - - // Lade Daten beim ersten Render oder wenn sich die PDF-ID ändert - useEffect(() => { - fetchKennzahlen(); - }, [pdfId]); - - // Funktion zum Senden der PUT-Anfrage - const updateKennzahl = async (label: string, value: string) => { - try { - const response = await fetch(`${API_BASE_URL}/api/kennzahlen/${label}?pdf_id=${pdfId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ value }) - }); - - if (!response.ok) { - throw new Error('Fehler beim Speichern der Kennzahl'); - } - - // Lade die Daten neu nach dem Update - await fetchKennzahlen(); - } catch (error) { - console.error('Fehler:', error); - setError('Fehler beim Speichern der Änderungen'); - } - }; - - // Bearbeitung starten - const startEditing = (value: string, index: number) => { - setEditingIndex(index); - setEditValue(value); - }; - - // Bearbeitung beenden und Wert speichern - const handleSave = async (index: number) => { - await updateKennzahl(rows[index].label, editValue); - setEditingIndex(null); - }; - - // Tastatureingaben verarbeiten - const handleKeyPress = (e: KeyboardEvent, index: number) => { - if (e.key === 'Enter') { - handleSave(index); - } else if (e.key === 'Escape') { - setEditingIndex(null); - } - }; - - if (isLoading) { - return
Lade Daten...
; - } - - if (error) { - return {error}; - } - - return ( - - - - - Kennzahl - Wert - Seite - - - - - {rows.map((row, index) => { - let borderColor = 'transparent'; - if (row.status === 'error') borderColor = 'red'; - else if (row.status === 'warning') borderColor = '#f6ed48'; - - return ( - - {row.label} - startEditing(row.value, index)}> - - - {row.status === 'error' && } - {row.status === 'warning' && } - {editingIndex === index ? ( - setEditValue(e.target.value)} - onKeyDown={(e) => handleKeyPress(e, index)} - onBlur={() => handleSave(index)} - autoFocus - size="small" - fullWidth - variant="standard" - sx={{ margin: '-8px 0' }} - /> - ) : ( - {row.value || '—'} - )} - - { - e.stopPropagation(); - startEditing(row.value, index); - }} - /> - - - - onPageClick?.(row.page)} - sx={{ cursor: 'pointer' }} - > - {row.page} - - - - ); - })} - -
-
- ); - } - \ No newline at end of file + Box, + IconButton, + Link, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, +} from "@mui/material"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useNavigate } from "@tanstack/react-router"; +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 + data: { + [key: string]: { + label: string; + entity: string; + page: number; + status: string; + source: string; + }[]; + }; +} + +// React-Komponente +export default function KennzahlenTable({ + onPageClick, + data, + pdfId, +}: KennzahlenTableProps) { + const [editingIndex, setEditingIndex] = useState(""); + const [editValue, setEditValue] = useState(""); + const navigate = useNavigate({ from: "/extractedResult/$pitchBook" }); + + const queryClient = useQueryClient(); + + const { mutate } = useMutation({ + mutationFn: (id: string) => { + const key = id.toUpperCase(); + const updatedData = { ...data }; + updatedData[key] = data[key]?.map((item) => ({ + ...item, + entity: editValue, + })) || [{ label: key, entity: editValue }]; + return fetchPutKPI(Number(pdfId), updatedData); + }, + onMutate: async (id: string) => { + await queryClient.cancelQueries({ + queryKey: ["pitchBookKPI", pdfId], + }); + + const snapshot = queryClient.getQueryData(["pitchBookKPI", pdfId]); + + const key = id.toUpperCase(); + + queryClient.setQueryData(["pitchBookKPI", pdfId], () => { + const updatedData = { ...data }; + updatedData[key] = data[key]?.map((item) => ({ + ...item, + entity: editValue, + })) || [{ label: key, entity: editValue }]; + return updatedData; + }); + + return () => { + queryClient.setQueryData(["pitchBookKPI", pdfId], snapshot); + }; + }, + onError: (error, _variables, rollback) => { + console.log("error", error); + rollback?.(); + }, + onSettled: () => { + return queryClient.invalidateQueries({ + queryKey: ["pitchBookKPI", pdfId], + }); + }, + }); + + // Bearbeitung starten + const startEditing = (value: string, index: string) => { + setEditingIndex(index); + setEditValue(value); + }; + + // Bearbeitung beenden und Wert speichern + const handleSave = async (index: string) => { + // await updateKennzahl(rows[index].label, editValue); + mutate(index); + setEditingIndex(""); + }; + + // Tastatureingaben verarbeiten + const handleKeyPress = (e: KeyboardEvent, index: string) => { + if (e.key === "Enter") { + handleSave(index); + } else if (e.key === "Escape") { + setEditingIndex("null"); + } + }; + + return ( + + + + + + Kennzahl + + + Wert + + + Seite + + + + + + {SETTINGS.filter((setting) => setting.active) + .sort((a, b) => a.position - b.position) + .map((setting) => ({ + setting: setting, + extractedValues: data[setting.name.toUpperCase()] || [], + })) + .map((row) => { + let borderColor = "transparent"; + if ( + row.setting.mandatory && + (row.extractedValues.length === 0 || + row.extractedValues.at(0)?.entity === "") + ) + borderColor = "red"; + else if (row.extractedValues.length > 1) borderColor = "#f6ed48"; + + return ( + + {row.setting.name} + + startEditing( + row.extractedValues.at(0)?.entity || "", + row.setting.name, + ) + } + > + + + {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.length <= 1 && ( + { + e.stopPropagation(); + startEditing( + row.extractedValues.at(0)?.entity || "", + row.setting.name, + ); + }} + /> + )} + + + + + onPageClick?.(Number(row.extractedValues.at(0)?.page)) + } + sx={{ cursor: "pointer" }} + > + {row.extractedValues.at(0)?.page} + + + + ); + })} + +
+
+ ); +} diff --git a/project/frontend/src/components/pdfViewer.tsx b/project/frontend/src/components/pdfViewer.tsx index 7d9c158..a24fa9f 100644 --- a/project/frontend/src/components/pdfViewer.tsx +++ b/project/frontend/src/components/pdfViewer.tsx @@ -12,7 +12,10 @@ interface PDFViewerProps { currentPage?: number; } -export default function PDFViewer({ pitchBookId, currentPage }: PDFViewerProps) { +export default function PDFViewer({ + pitchBookId, + currentPage, +}: PDFViewerProps) { const [numPages, setNumPages] = useState(null); const [pageNumber, setPageNumber] = useState(currentPage || 1); const [containerWidth, setContainerWidth] = useState(null); @@ -39,7 +42,7 @@ export default function PDFViewer({ pitchBookId, currentPage }: PDFViewerProps) if (currentPage && currentPage !== pageNumber) { setPageNumber(currentPage); } - }, [currentPage]); + }, [currentPage, pageNumber]); useEffect(() => { const handleProgress = (data: { id: number; progress: number }) => { @@ -115,4 +118,4 @@ export default function PDFViewer({ pitchBookId, currentPage }: PDFViewerProps) ); -} \ No newline at end of file +} diff --git a/project/frontend/src/routeTree.gen.ts b/project/frontend/src/routeTree.gen.ts index b3c2de7..7338b4e 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 ExtractedResultPitchBookKpiImport } from './routes/extractedResult_.$pitchBook.$kpi' // Create/Update Routes @@ -35,6 +36,13 @@ const ExtractedResultPitchBookRoute = ExtractedResultPitchBookImport.update({ getParentRoute: () => rootRoute, } as any) +const ExtractedResultPitchBookKpiRoute = + ExtractedResultPitchBookKpiImport.update({ + id: '/extractedResult_/$pitchBook/$kpi', + path: '/extractedResult/$pitchBook/$kpi', + getParentRoute: () => rootRoute, + } as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -60,6 +68,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ExtractedResultPitchBookImport parentRoute: typeof rootRoute } + '/extractedResult_/$pitchBook/$kpi': { + id: '/extractedResult_/$pitchBook/$kpi' + path: '/extractedResult/$pitchBook/$kpi' + fullPath: '/extractedResult/$pitchBook/$kpi' + preLoaderRoute: typeof ExtractedResultPitchBookKpiImport + parentRoute: typeof rootRoute + } } } @@ -69,12 +84,14 @@ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/config': typeof ConfigRoute '/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute + '/extractedResult/$pitchBook/$kpi': typeof ExtractedResultPitchBookKpiRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/config': typeof ConfigRoute '/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute + '/extractedResult/$pitchBook/$kpi': typeof ExtractedResultPitchBookKpiRoute } export interface FileRoutesById { @@ -82,14 +99,28 @@ export interface FileRoutesById { '/': typeof IndexRoute '/config': typeof ConfigRoute '/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute + '/extractedResult_/$pitchBook/$kpi': typeof ExtractedResultPitchBookKpiRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/config' | '/extractedResult/$pitchBook' + fullPaths: + | '/' + | '/config' + | '/extractedResult/$pitchBook' + | '/extractedResult/$pitchBook/$kpi' fileRoutesByTo: FileRoutesByTo - to: '/' | '/config' | '/extractedResult/$pitchBook' - id: '__root__' | '/' | '/config' | '/extractedResult/$pitchBook' + to: + | '/' + | '/config' + | '/extractedResult/$pitchBook' + | '/extractedResult/$pitchBook/$kpi' + id: + | '__root__' + | '/' + | '/config' + | '/extractedResult/$pitchBook' + | '/extractedResult_/$pitchBook/$kpi' fileRoutesById: FileRoutesById } @@ -97,12 +128,14 @@ export interface RootRouteChildren { IndexRoute: typeof IndexRoute ConfigRoute: typeof ConfigRoute ExtractedResultPitchBookRoute: typeof ExtractedResultPitchBookRoute + ExtractedResultPitchBookKpiRoute: typeof ExtractedResultPitchBookKpiRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, ConfigRoute: ConfigRoute, ExtractedResultPitchBookRoute: ExtractedResultPitchBookRoute, + ExtractedResultPitchBookKpiRoute: ExtractedResultPitchBookKpiRoute, } export const routeTree = rootRoute @@ -117,7 +150,8 @@ export const routeTree = rootRoute "children": [ "/", "/config", - "/extractedResult/$pitchBook" + "/extractedResult/$pitchBook", + "/extractedResult_/$pitchBook/$kpi" ] }, "/": { @@ -128,6 +162,9 @@ export const routeTree = rootRoute }, "/extractedResult/$pitchBook": { "filePath": "extractedResult.$pitchBook.tsx" + }, + "/extractedResult_/$pitchBook/$kpi": { + "filePath": "extractedResult_.$pitchBook.$kpi.tsx" } } } diff --git a/project/frontend/src/routes/extractedResult.$pitchBook.tsx b/project/frontend/src/routes/extractedResult.$pitchBook.tsx index 70d83df..db7c47e 100644 --- a/project/frontend/src/routes/extractedResult.$pitchBook.tsx +++ b/project/frontend/src/routes/extractedResult.$pitchBook.tsx @@ -1,12 +1,16 @@ import ContentPasteIcon from "@mui/icons-material/ContentPaste"; import { Box, Button, Paper, Typography } from "@mui/material"; +import { useSuspenseQuery } from "@tanstack/react-query"; 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"; export const Route = createFileRoute("/extractedResult/$pitchBook")({ component: ExtractedResultsPage, + loader: ({ context: { queryClient }, params: { pitchBook } }) => + queryClient.ensureQueryData(kpiQueryOptions(pitchBook)), }); function ExtractedResultsPage() { @@ -21,6 +25,8 @@ function ExtractedResultsPage() { green: "#3fd942", }[status]; + const { data: kpi } = useSuspenseQuery(kpiQueryOptions(pitchBook)); + return ( @@ -60,7 +66,11 @@ function ExtractedResultsPage() { overflow: "auto", }} > - + + queryClient.ensureQueryData(kpiQueryOptions(pitchBook)), +}); + +function RouteComponent() { + const { pitchBook, kpi } = Route.useParams(); + + const { + data: { [kpi.toUpperCase()]: kpiValues }, + } = useSuspenseQuery(kpiQueryOptions(pitchBook)); + + return ( +
+ {kpiValues.map((e) => ( +
+ {e.label}: {e.entity} +
+ ))} +
+ ); +} diff --git a/project/frontend/src/util/api.ts b/project/frontend/src/util/api.ts new file mode 100644 index 0000000..8e35d2e --- /dev/null +++ b/project/frontend/src/util/api.ts @@ -0,0 +1,97 @@ +export const fetchKPI = async ( + pitchBookId: string, +): Promise<{ + [key: string]: { + label: string; + entity: string; + page: number; + status: string; + source: string; + }[]; +}> => { + const response = await fetch( + `http://localhost:5050/api/pitch_book/${pitchBookId}`, + ); + const data = await response.json(); + + return getKPI(data.kpi); +}; + +export const fetchPutKPI = async ( + pitchBookId: number, + kpi: { + [key: string]: { + label: string; + entity: string; + page: number; + status: string; + source: string; + }[]; + }, +): Promise<{ + [key: string]: { + label: string; + entity: string; + page: number; + status: string; + source: string; + }[]; +}> => { + const formData = new FormData(); + formData.append("kpi", JSON.stringify(flattenKPIArray(kpi))); + + const response = await fetch( + `http://localhost:5050/api/pitch_book/${pitchBookId}`, + { + method: "PUT", + body: formData, + }, + ); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + return getKPI(data.kpi); +}; + +const getKPI = (data: string) => { + const kpi = JSON.parse(data) as { + label: string; + entity: string; + page: number; + status: string; + source: string; + }[]; + + const reducedKpi = kpi.reduce( + (prev, curr) => { + if (!prev[curr.label]) { + prev[curr.label] = []; + } + prev[curr.label].push(curr); + return prev; + }, + {} as { + [key: string]: { + label: string; + entity: string; + page: number; + status: string; + source: string; + }[]; + }, + ); + return reducedKpi; +}; + +export const flattenKPIArray = (kpi: { + [key: string]: { + label: string; + entity: string; + page: number; + status: string; + source: string; + }[]; +}) => { + return Object.values(kpi).flat(); +}; diff --git a/project/frontend/src/util/query.ts b/project/frontend/src/util/query.ts new file mode 100644 index 0000000..0eaeef8 --- /dev/null +++ b/project/frontend/src/util/query.ts @@ -0,0 +1,8 @@ +import { queryOptions } from "@tanstack/react-query"; +import { fetchKPI } from "./api"; + +export const kpiQueryOptions = (pitchBookId: string) => + queryOptions({ + queryKey: ["pitchBookKPI", pitchBookId], + queryFn: () => fetchKPI(pitchBookId), + });