diff --git a/project/frontend/src/components/KennzahlenTable.tsx b/project/frontend/src/components/KennzahlenTable.tsx index b6a19bb..e01e98e 100644 --- a/project/frontend/src/components/KennzahlenTable.tsx +++ b/project/frontend/src/components/KennzahlenTable.tsx @@ -42,25 +42,43 @@ export default function KennzahlenTable({ data, pdfId, settings, - from + from, }: KennzahlenTableProps) { const [editingIndex, setEditingIndex] = useState(""); const [editValue, setEditValue] = useState(""); + const [editingPageIndex, setEditingPageIndex] = useState(""); + const [editPageValue, setEditPageValue] = useState(""); + const [hoveredPageIndex, setHoveredPageIndex] = useState(""); const navigate = useNavigate(); const queryClient = useQueryClient(); const { mutate } = useMutation({ - mutationFn: (id: string) => { + mutationFn: (params: { id: string; newValue?: string; newPage?: number }) => { + const { id, newValue, newPage } = params; const key = id.toUpperCase(); const updatedData = { ...data }; - updatedData[key] = data[key]?.map((item) => ({ - ...item, - entity: editValue, - })) || [{ label: key, entity: editValue }]; + + if (data[key] && data[key].length > 0) { + updatedData[key] = data[key].map((item) => ({ + ...item, + ...(newValue !== undefined && { entity: newValue }), + ...(newPage !== undefined && { page: newPage }), + })); + } else { + updatedData[key] = [{ + label: key, + entity: newValue || "", + page: newPage || 0, + status: "single-source", + source: "manual" + }]; + } + return fetchPutKPI(Number(pdfId), updatedData); }, - onMutate: async (id: string) => { + onMutate: async (params: { id: string; newValue?: string; newPage?: number }) => { + const { id, newValue, newPage } = params; await queryClient.cancelQueries({ queryKey: ["pitchBookKPI", pdfId], }); @@ -71,10 +89,23 @@ export default function KennzahlenTable({ queryClient.setQueryData(["pitchBookKPI", pdfId], () => { const updatedData = { ...data }; - updatedData[key] = data[key]?.map((item) => ({ - ...item, - entity: editValue, - })) || [{ label: key, entity: editValue }]; + + if (data[key] && data[key].length > 0) { + updatedData[key] = data[key].map((item) => ({ + ...item, + ...(newValue !== undefined && { entity: newValue }), + ...(newPage !== undefined && { page: newPage }), + })); + } else { + updatedData[key] = [{ + label: key, + entity: newValue || "", + page: newPage || 0, + status: "single-source", + source: "manual" + }]; + } + return updatedData; }); @@ -99,19 +130,39 @@ export default function KennzahlenTable({ setEditValue(value); }; + const startPageEditing = (value: number, index: string) => { + setEditingPageIndex(index); + setEditPageValue(value.toString()); + }; + // Bearbeitung beenden und Wert speichern const handleSave = async (index: string) => { - // await updateKennzahl(rows[index].label, editValue); - mutate(index); + mutate({ id: index, newValue: editValue }); setEditingIndex(""); }; + const handlePageSave = async (index: string) => { + const pageNumber = parseInt(editPageValue); + if (!isNaN(pageNumber) && pageNumber > 0) { + mutate({ id: index, newPage: pageNumber }); + } + setEditingPageIndex(""); + }; + // Tastatureingaben verarbeiten const handleKeyPress = (e: KeyboardEvent, index: string) => { if (e.key === "Enter") { handleSave(index); } else if (e.key === "Escape") { - setEditingIndex("null"); + setEditingIndex(""); + } + }; + + const handlePageKeyPress = (e: KeyboardEvent, index: string) => { + if (e.key === "Enter") { + handlePageSave(index); + } else if (e.key === "Escape") { + setEditingPageIndex(""); } }; @@ -131,14 +182,16 @@ export default function KennzahlenTable({ - + Kennzahl - + Wert - - Seite + + + Seite + @@ -165,6 +218,10 @@ export default function KennzahlenTable({ borderColor = "#f6ed48"; } + const currentPage = row.extractedValues.at(0)?.page ?? 0; + const isPageHovered = hoveredPageIndex === row.setting.name; + const canEditPage = !hasMultipleValues; + return ( {row.setting.name} @@ -229,12 +286,17 @@ export default function KennzahlenTable({ ) : ( - Problem -
- Es wurden keine Kennzahlen gefunden. Bitte ergänzen! - : "" + title={ + hasNoValue ? ( + <> + Problem +
+ Es wurden keine Kennzahlen gefunden. Bitte + ergänzen! + + ) : ( + "" + ) } placement="bottom" arrow @@ -261,7 +323,10 @@ export default function KennzahlenTable({ }} > {hasNoValue && ( - + )} {editingIndex === row.setting.name ? ( - {(row.extractedValues.at(0)?.page ?? 0) > 0 ? ( - { - const extractedValue = row.extractedValues.at(0); - if (extractedValue?.page && extractedValue.page > 0) { - onPageClick?.(Number(extractedValue.page), extractedValue.entity || ""); + {editingPageIndex === row.setting.name ? ( + { + const value = e.target.value; + if (value === '' || /^\d+$/.test(value) && parseInt(value) > 0) { + setEditPageValue(value); } }} - sx={{ cursor: "pointer" }} - > - {row.extractedValues.at(0)?.page} - + onKeyDown={(e) => handlePageKeyPress(e, row.setting.name)} + onBlur={() => handlePageSave(row.setting.name)} + autoFocus + size="small" + variant="standard" + sx={{ + width: "60px", + "& .MuiInput-input": { + textAlign: "center" + } + }} + inputProps={{ + min: 0, + style: { textAlign: 'center' } + }} + /> ) : ( - "" + <> + {currentPage > 0 ? ( + canEditPage && setHoveredPageIndex(row.setting.name)} + onMouseLeave={() => setHoveredPageIndex("")} + onClick={() => { + if (canEditPage) { + startPageEditing(currentPage, row.setting.name); + } + }} + > + { + e.stopPropagation(); + const extractedValue = row.extractedValues.at(0); + if (extractedValue?.page && extractedValue.page > 0) { + onPageClick?.(Number(extractedValue.page), extractedValue.entity || ""); + } + }} + sx={{ cursor: "pointer" }} + > + {currentPage} + + + {isPageHovered && canEditPage && ( + + )} + + ) : canEditPage ? ( + setHoveredPageIndex(row.setting.name)} + onMouseLeave={() => setHoveredPageIndex("")} + onClick={() => startPageEditing(0, row.setting.name)} + > + ... + + + ) : ( + "" + )} + )}
@@ -324,4 +483,4 @@ export default function KennzahlenTable({
); -} +} \ No newline at end of file diff --git a/project/frontend/src/components/PitchBooksTable.tsx b/project/frontend/src/components/PitchBooksTable.tsx index 7a86ada..1398a18 100644 --- a/project/frontend/src/components/PitchBooksTable.tsx +++ b/project/frontend/src/components/PitchBooksTable.tsx @@ -1,186 +1,388 @@ -import { Box, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, CircularProgress, Chip } from "@mui/material"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import HourglassEmptyIcon from "@mui/icons-material/HourglassEmpty"; +import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; +import { + Box, + Chip, + CircularProgress, + LinearProgress, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; +import { useCallback, useEffect, useState } from "react"; +import { socket } from "../socket"; +import { fetchPitchBooksById } from "../util/api"; import { pitchBooksQueryOptions } from "../util/query"; -import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'; interface PitchBook { - id: number; - filename: string; - created_at: string; - kpi?: string | { - [key: string]: { - label: string; - entity: string; - page: number; - status: string; - source: string; - }[]; - }; - status?: 'processing' | 'completed'; + id: number; + filename: string; + created_at: string; + kpi?: + | string + | { + [key: string]: { + label: string; + entity: string; + page: number; + status: string; + source: string; + }[]; + }; + status?: "processing" | "completed"; } export function PitchBooksTable() { - const navigate = useNavigate(); - const { data: pitchBooks, isLoading } = useSuspenseQuery(pitchBooksQueryOptions()); + const [loadingPitchBooks, setLoadingPitchBooks] = useState< + { + id: number; + progress: number; + filename?: string; + buffer: number; + intervalId?: number; + }[] + >([]); + const navigate = useNavigate(); + const { data: pitchBooks, isLoading } = useSuspenseQuery( + pitchBooksQueryOptions(), + ); - const handleRowClick = (pitchBookId: number) => { - navigate({ - to: "/extractedResult/$pitchBook", - params: { pitchBook: pitchBookId.toString() }, - search: { from: "overview" } - }); - }; + const handleRowClick = (pitchBookId: number) => { + navigate({ + to: "/extractedResult/$pitchBook", + params: { pitchBook: pitchBookId.toString() }, + search: { from: "overview" }, + }); + }; - const getKPIValue = (pitchBook: PitchBook, fieldName: string): string => { - if (!pitchBook.kpi || typeof pitchBook.kpi === 'string') { - try { - const parsedKPI = JSON.parse(pitchBook.kpi as string); - // Convert array to object format if needed - const kpiObj = Array.isArray(parsedKPI) ? - parsedKPI.reduce((acc: any, item: any) => { - if (!acc[item.label]) acc[item.label] = []; - acc[item.label].push(item); - return acc; - }, {}) : parsedKPI; + const onConnection = useCallback(() => { + console.log("connected"); + }, []); - return kpiObj[fieldName]?.[0]?.entity || 'N/A'; - } catch { - return 'N/A'; - } - } + const queryClient = useQueryClient(); - return (pitchBook.kpi as any)[fieldName]?.[0]?.entity || 'N/A'; - }; + const onProgress = useCallback( + (progress: { id: number; progress: number }) => { + if (progress.progress === 100) { + setLoadingPitchBooks((prev) => { + const intervalId = prev.find( + (item) => item.id === progress.id, + )?.intervalId; + console.log(intervalId, prev); + intervalId && clearInterval(intervalId); - const getStatus = (pitchBook: PitchBook) => { - if (pitchBook.kpi && - ((typeof pitchBook.kpi === 'string' && pitchBook.kpi !== '{}') || - (typeof pitchBook.kpi === 'object' && Object.keys(pitchBook.kpi).length > 0))) { - return 'completed'; - } - return 'processing'; - }; + return [...prev.filter((item) => item.id !== progress.id)]; + }); + queryClient.invalidateQueries({ + queryKey: pitchBooksQueryOptions().queryKey, + }); + } else { + setLoadingPitchBooks((prev) => { + const oldItem = prev.find((item) => item.id === progress.id); + let intervalId = oldItem?.intervalId; + if (!oldItem) { + intervalId = setInterval(() => { + setLoadingPitchBooks((prev) => { + const oldItem = prev.find((item) => item.id === progress.id); + if (!oldItem) return prev; - if (isLoading) { - return ( - - - - ); - } + return [ + ...prev.filter((e) => e.id !== progress.id), + { + id: progress.id, + progress: oldItem?.progress ?? progress.progress, + filename: oldItem?.filename, + buffer: oldItem ? oldItem.buffer + 0.5 : 0, + intervalId: oldItem.intervalId, + }, + ]; + }); + }, 400); - return ( - - - - - - Fondsname - Fondsmanager - Dateiname - Status - - - - {pitchBooks.map((pitchBook: PitchBook) => { - const status = getStatus(pitchBook); - const fundName = getKPIValue(pitchBook, 'FONDSNAME') || - getKPIValue(pitchBook, 'FUND_NAME') || - getKPIValue(pitchBook, 'NAME'); + fetchPitchBooksById(progress.id) + .then((res) => { + setLoadingPitchBooks((prev) => [ + ...prev.filter((item) => item.id !== progress.id), + { + id: progress.id, + progress: progress.progress, + filename: res.filename, + buffer: 0, + intervalId, + }, + ]); + }) + .catch((err) => { + console.error(err); + }); + } + return [ + ...prev.filter((item) => item.id !== progress.id), + { + id: progress.id, + progress: progress.progress, + filename: oldItem?.filename, + buffer: 0, + intervalId, + }, + ]; + }); + } + }, + [queryClient], + ); - const manager = getKPIValue(pitchBook, 'FONDSMANAGER') || - getKPIValue(pitchBook, 'MANAGER') || - getKPIValue(pitchBook, 'PORTFOLIO_MANAGER'); + useEffect(() => { + socket.on("connect", onConnection); + socket.on("progress", onProgress); + return () => { + socket.off("connect", onConnection); + socket.off("progress", onProgress); + }; + }, [onConnection, onProgress]); - return ( - handleRowClick(pitchBook.id)} - sx={{ - cursor: "pointer", - "&:hover": { - backgroundColor: "#f9f9f9", - }, - }} - > - - - - - - - - {fundName} - - - {manager} - - - {pitchBook.filename} - - - - {status === 'completed' ? ( - } - label="Abgeschlossen" - size="small" - sx={{ - backgroundColor: "#e8f5e9", - color: "#2e7d32", - "& .MuiChip-icon": { - color: "#2e7d32", - }, - }} - /> - ) : ( - } - label="In Bearbeitung" - size="small" - sx={{ - backgroundColor: "#fff3e0", - color: "#e65100", - "& .MuiChip-icon": { - color: "#e65100", - }, - }} - /> - )} - - - ); - })} - -
- {pitchBooks.length === 0 && ( - - - Keine Pitch Books vorhanden - - - )} -
- ); -} \ No newline at end of file + const getKPIValue = (pitchBook: PitchBook, fieldName: string): string => { + if (!pitchBook.kpi || typeof pitchBook.kpi === "string") { + try { + const parsedKPI = JSON.parse(pitchBook.kpi as string); + // Convert array to object format if needed + const kpiObj = Array.isArray(parsedKPI) + ? parsedKPI.reduce((acc, item) => { + if (!acc[item.label]) acc[item.label] = []; + acc[item.label].push(item); + return acc; + }, {}) + : parsedKPI; + + return kpiObj[fieldName]?.[0]?.entity || "N/A"; + } catch { + return "N/A"; + } + } + + return pitchBook.kpi[fieldName]?.[0]?.entity || "N/A"; + }; + + const getStatus = (pitchBook: PitchBook) => { + if ( + pitchBook.kpi && + ((typeof pitchBook.kpi === "string" && pitchBook.kpi !== "{}") || + (typeof pitchBook.kpi === "object" && + Object.keys(pitchBook.kpi).length > 0)) + ) { + return "completed"; + } + return "processing"; + }; + + if (isLoading) { + return ( + + + + ); + } + + return ( + + + + + + Fondsname + Fondsmanager + Dateiname + + Status + + + + + {pitchBooks + .filter( + (pitchbook: PitchBook) => + !loadingPitchBooks.some((e) => e.id === pitchbook.id), + ) + .sort( + (a: PitchBook, b: PitchBook) => + new Date(a.created_at).getTime() - + new Date(b.created_at).getTime(), + ) + .map((pitchBook: PitchBook) => { + const status = getStatus(pitchBook); + const fundName = + getKPIValue(pitchBook, "FONDSNAME") || + getKPIValue(pitchBook, "FUND_NAME") || + getKPIValue(pitchBook, "NAME"); + + const manager = + getKPIValue(pitchBook, "FONDSMANAGER") || + getKPIValue(pitchBook, "MANAGER") || + getKPIValue(pitchBook, "PORTFOLIO_MANAGER"); + + return ( + handleRowClick(pitchBook.id)} + sx={{ + cursor: "pointer", + "&:hover": { + backgroundColor: "#f9f9f9", + }, + }} + > + + + + + + + + {fundName} + + + {manager} + + + {pitchBook.filename} + + + + {status === "completed" ? ( + } + label="Abgeschlossen" + size="small" + sx={{ + backgroundColor: "#e8f5e9", + color: "#2e7d32", + "& .MuiChip-icon": { + color: "#2e7d32", + }, + }} + /> + ) : ( + } + label="In Bearbeitung" + size="small" + sx={{ + backgroundColor: "#fff3e0", + color: "#e65100", + "& .MuiChip-icon": { + color: "#e65100", + }, + }} + /> + )} + + + ); + })} + {loadingPitchBooks + .sort((a, b) => a.id - b.id) + .map((pitchBook) => ( + + + + + + + + + + + {" "} + + {pitchBook.filename} + + + + } + label="In Bearbeitung" + size="small" + sx={{ + backgroundColor: "#fff3e0", + color: "#e65100", + "& .MuiChip-icon": { + color: "#e65100", + }, + }} + /> + + + ))} + +
+ {pitchBooks.length === 0 && ( + + + Keine Pitch Books vorhanden + + + )} +
+ ); +} diff --git a/project/frontend/src/components/UploadPage.tsx b/project/frontend/src/components/UploadPage.tsx index 6de8a62..e72c9da 100644 --- a/project/frontend/src/components/UploadPage.tsx +++ b/project/frontend/src/components/UploadPage.tsx @@ -1,11 +1,11 @@ import SettingsIcon from "@mui/icons-material/Settings"; import { Backdrop, Box, Button, IconButton, Paper } from "@mui/material"; -import { useNavigate } from "@tanstack/react-router"; +import { useNavigate, useRouter } from "@tanstack/react-router"; import { useCallback, useEffect, useState } from "react"; import FileUpload from "react-material-file-upload"; import { socket } from "../socket"; -import { CircularProgressWithLabel } from "./CircularProgressWithLabel"; import { API_HOST } from "../util/api"; +import { CircularProgressWithLabel } from "./CircularProgressWithLabel"; export default function UploadPage() { const [files, setFiles] = useState([]); @@ -13,6 +13,7 @@ export default function UploadPage() { const [loadingState, setLoadingState] = useState(null); const fileTypes = ["pdf"]; const navigate = useNavigate(); + const router = useRouter(); const uploadFile = useCallback(async () => { const formData = new FormData(); @@ -178,6 +179,7 @@ export default function UploadPage() { backgroundColor: "#383838", "&:hover": { backgroundColor: "#2e2e2e" }, }} + onMouseEnter={() => router.preloadRoute({ to: "/pitchbooks" })} onClick={() => navigate({ to: "/pitchbooks" })} > Alle Pitch Books anzeigen diff --git a/project/frontend/src/util/api.ts b/project/frontend/src/util/api.ts index 65d3b2e..ca066c1 100644 --- a/project/frontend/src/util/api.ts +++ b/project/frontend/src/util/api.ts @@ -1,6 +1,6 @@ import type { Kennzahl } from "@/types/kpi"; -const API_HOST = import.meta.env.VITE_API_HOST || 'http://localhost:5050'; +const API_HOST = import.meta.env.VITE_API_HOST || "http://localhost:5050"; export { API_HOST }; @@ -15,9 +15,7 @@ export const fetchKPI = async ( source: string; }[]; }> => { - const response = await fetch( - `${API_HOST}/api/pitch_book/${pitchBookId}`, - ); + const response = await fetch(`${API_HOST}/api/pitch_book/${pitchBookId}`); const data = await response.json(); return data.kpi ? getKPI(data.kpi) : {}; @@ -46,13 +44,10 @@ export const fetchPutKPI = async ( const formData = new FormData(); formData.append("kpi", JSON.stringify(flattenKPIArray(kpi))); - const response = await fetch( - `${API_HOST}/api/pitch_book/${pitchBookId}`, - { - method: "PUT", - body: formData, - }, - ); + const response = await fetch(`${API_HOST}/api/pitch_book/${pitchBookId}`, { + method: "PUT", + body: formData, + }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -119,3 +114,11 @@ export async function fetchPitchBooks() { } return response.json(); } + +export async function fetchPitchBooksById(id: number) { + const response = await fetch(`${API_HOST}/api/pitch_book/${id}`); + if (!response.ok) { + throw new Error("Failed to fetch pitch books"); + } + return response.json(); +}