frontend/31-highlight-kennzahlen #47

Closed
1924466 wants to merge 8 commits from frontend/31-highlight-kennzahlen into main
2 changed files with 234 additions and 243 deletions

View File

@ -3,44 +3,37 @@ import {
TableHead, TableRow, Paper, Box, TableHead, TableRow, Paper, Box,
Dialog, DialogActions, DialogContent, DialogTitle, Dialog, DialogActions, DialogContent, DialogTitle,
TextField, Button, Link TextField, Button, Link
} from '@mui/material'; } from '@mui/material';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import { useState } from 'react'; import { useState } from 'react';
const exampleData = [
// Beispiel-Daten
const exampleData = [
{ label: 'Fondsname', value: 'Fund Real Estate Prime Europe', page: 1, status: 'ok' }, { label: 'Fondsname', value: 'Fund Real Estate Prime Europe', page: 1, status: 'ok' },
{ label: 'Fondsmanager', value: '', page: 1, status: 'error' }, { label: 'Fondsmanager', value: '', page: 1, status: 'error' },
{ label: 'Risikoprofil', value: 'Core/Core+', page: 10, status: 'warning' }, { label: 'Risikoprofil', value: 'Core/Core+', page: 10, status: 'warning' },
{ label: 'LTV', value: '30-35 %', page: 8, status: 'ok' }, { label: 'LTV', value: '30-35 %', page: 8, status: 'ok' },
{ label: 'Ausschüttungsrendite', value: '4%', page: 34, status: 'ok' } { label: 'Ausschüttungsrendite', value: '4%', page: 34, status: 'ok' }
]; ];
interface KennzahlenTableProps { interface KennzahlenTableProps {
onPageClick?: (page: number) => void; onPageClick?: (page: number) => void;
} setSelectedLabel?: (label: string) => void;
}
// React-Komponente export default function KennzahlenTable({ onPageClick, setSelectedLabel }: KennzahlenTableProps) {
export default function KennzahlenTable({ onPageClick }: KennzahlenTableProps) {
// Zustand für bearbeitbare Daten
const [rows, setRows] = useState(exampleData); const [rows, setRows] = useState(exampleData);
const [open, setOpen] = useState(false);
const [currentValue, setCurrentValue] = useState('');
const [currentIndex, setCurrentIndex] = useState<number | null>(null);
// Zustände für Dialog-Funktion
const [open, setOpen] = useState(false); // Dialog anzeigen?
const [currentValue, setCurrentValue] = useState(''); // Eingabewert
const [currentIndex, setCurrentIndex] = useState<number | null>(null); // Zeilenindex
// Beim Klick auf das Stift-Icon: Dialog öffnen
const handleEditClick = (value: string, index: number) => { const handleEditClick = (value: string, index: number) => {
setCurrentValue(value); setCurrentValue(value);
setCurrentIndex(index); setCurrentIndex(index);
setOpen(true); setOpen(true);
}; };
// Wert speichern und Dialog schließen
const handleSave = () => { const handleSave = () => {
if (currentIndex !== null) { if (currentIndex !== null) {
const updated = [...rows]; const updated = [...rows];
@ -54,7 +47,6 @@ import {
<> <>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table> <Table>
{/* Tabellenkopf */}
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell><strong>Kennzahl</strong></TableCell> <TableCell><strong>Kennzahl</strong></TableCell>
@ -62,21 +54,20 @@ import {
<TableCell><strong>Seite</strong></TableCell> <TableCell><strong>Seite</strong></TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
{/* Tabelleninhalt */}
<TableBody> <TableBody>
{rows.map((row, index) => { {rows.map((row, index) => {
// Rahmenfarbe anhand Status
let borderColor = 'transparent'; let borderColor = 'transparent';
if (row.status === 'error') borderColor = 'red'; if (row.status === 'error') borderColor = 'red';
else if (row.status === 'warning') borderColor = '#f6ed48'; else if (row.status === 'warning') borderColor = '#f6ed48';
return ( return (
<TableRow key={index}> <TableRow
{/* Kennzahl */} key={index}
onClick={() => setSelectedLabel?.(row.label)}
hover
sx={{ cursor: 'pointer' }}
>
<TableCell>{row.label}</TableCell> <TableCell>{row.label}</TableCell>
{/* Wert mit Status-Icons + Stift rechts */}
<TableCell> <TableCell>
<Box <Box
sx={{ sx={{
@ -94,8 +85,6 @@ import {
{row.status === 'warning' && <SearchIcon fontSize="small" sx={{ color: '#f6ed48' }} />} {row.status === 'warning' && <SearchIcon fontSize="small" sx={{ color: '#f6ed48' }} />}
<span>{row.value || '—'}</span> <span>{row.value || '—'}</span>
</Box> </Box>
{/* Stift-Icon */}
<EditIcon <EditIcon
fontSize="small" fontSize="small"
sx={{ color: '#555', cursor: 'pointer' }} sx={{ color: '#555', cursor: 'pointer' }}
@ -103,8 +92,6 @@ import {
/> />
</Box> </Box>
</TableCell> </TableCell>
{/* Seitenzahl */}
<TableCell> <TableCell>
<Link <Link
component="button" component="button"
@ -121,7 +108,6 @@ import {
</Table> </Table>
</TableContainer> </TableContainer>
{/* Dialog zum Bearbeiten */}
<Dialog open={open} onClose={() => setOpen(false)}> <Dialog open={open} onClose={() => setOpen(false)}>
<DialogTitle>Kennzahl bearbeiten</DialogTitle> <DialogTitle>Kennzahl bearbeiten</DialogTitle>
<DialogContent> <DialogContent>
@ -141,5 +127,4 @@ import {
</Dialog> </Dialog>
</> </>
); );
} }

View File

@ -1,12 +1,17 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState, useCallback } from "react";
import { Document, Page } from "react-pdf"; import { Document, Page, pdfjs } from "react-pdf";
import "react-pdf/dist/esm/Page/AnnotationLayer.css"; import "react-pdf/dist/esm/Page/AnnotationLayer.css";
import "react-pdf/dist/esm/Page/TextLayer.css"; import "react-pdf/dist/esm/Page/TextLayer.css";
import { Box, IconButton } from "@mui/material";
import ArrowCircleLeftIcon from "@mui/icons-material/ArrowCircleLeft"; import ArrowCircleLeftIcon from "@mui/icons-material/ArrowCircleLeft";
import ArrowCircleRightIcon from "@mui/icons-material/ArrowCircleRight"; import ArrowCircleRightIcon from "@mui/icons-material/ArrowCircleRight";
import { Box, IconButton } from "@mui/material";
import { socket } from "../socket"; import { socket } from "../socket";
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.mjs",
import.meta.url
).toString();
interface PDFViewerProps { interface PDFViewerProps {
pitchBookId: string; pitchBookId: string;
currentPage?: number; currentPage?: number;
@ -16,55 +21,68 @@ export default function PDFViewer({ pitchBookId, currentPage }: PDFViewerProps)
const [numPages, setNumPages] = useState<number | null>(null); const [numPages, setNumPages] = useState<number | null>(null);
const [pageNumber, setPageNumber] = useState(currentPage || 1); const [pageNumber, setPageNumber] = useState(currentPage || 1);
const [containerWidth, setContainerWidth] = useState<number | null>(null); const [containerWidth, setContainerWidth] = useState<number | null>(null);
const [highlightLabels, setHighlightLabels] = useState<string[]>([]);
const [pdfKey, setPdfKey] = useState(Date.now()); const [pdfKey, setPdfKey] = useState(Date.now());
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => { const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
setNumPages(numPages); setNumPages(numPages);
}; };
// Resize
useEffect(() => { useEffect(() => {
const updateWidth = () => { const updateWidth = () => {
if (containerRef.current) { if (containerRef.current) {
setContainerWidth(containerRef.current.offsetWidth); setContainerWidth(containerRef.current.offsetWidth);
} }
}; };
updateWidth(); updateWidth();
window.addEventListener("resize", updateWidth); window.addEventListener("resize", updateWidth);
return () => window.removeEventListener("resize", updateWidth); return () => window.removeEventListener("resize", updateWidth);
}, []); }, []);
// Highlight-Wörter setzen
useEffect(() => {
setHighlightLabels(["LTV", "Fondsmanager", "Risikoprofil"]);
}, []);
// Highlight-Logik
const highlightPattern = (text: string, patterns: string[]) => {
for (const word of patterns) {
const regex = new RegExp(`(${word})`, "gi");
text = text.replace(regex, "<mark>$1</mark>");
}
return text;
};
const textRenderer = useCallback(
(textItem: { str: string }) => highlightPattern(textItem.str, highlightLabels),
[highlightLabels]
);
// Seitenwechsel bei Prop-Änderung
useEffect(() => { useEffect(() => {
if (currentPage && currentPage !== pageNumber) { if (currentPage && currentPage !== pageNumber) {
setPageNumber(currentPage); setPageNumber(currentPage);
} }
}, [currentPage]); }, [currentPage]);
// Socket: Re-Render bei Fortschritt
useEffect(() => { useEffect(() => {
const handleProgress = (data: { id: number; progress: number }) => { const handleProgress = (data: { id: number; progress: number }) => {
if (data.id.toString() === pitchBookId && data.progress === 50) { if (data.id.toString() === pitchBookId && data.progress === 50) {
setPdfKey(Date.now()); setPdfKey(Date.now()); // Force re-render
} }
}; };
socket.on("progress", handleProgress); socket.on("progress", handleProgress);
return () => { return () => {
socket.off("progress", handleProgress); socket.off("progress", handleProgress);
}; };
}, [pitchBookId]); }, [pitchBookId]);
return ( return (
<Box <Box display="flex" flexDirection="column" justifyContent="center" alignItems="center" width="100%" height="100%" p={2}>
display="flex"
flexDirection="column"
justifyContent="center"
alignItems="center"
width="100%"
height="100%"
p={2}
>
<Box <Box
ref={containerRef} ref={containerRef}
sx={{ sx={{
@ -80,36 +98,24 @@ export default function PDFViewer({ pitchBookId, currentPage }: PDFViewerProps)
key={pdfKey} key={pdfKey}
file={`http://localhost:5050/api/pitch_book/${pitchBookId}/download`} file={`http://localhost:5050/api/pitch_book/${pitchBookId}/download`}
onLoadSuccess={onDocumentLoadSuccess} onLoadSuccess={onDocumentLoadSuccess}
onLoadError={(error) => onLoadError={(error) => console.error("Fehler beim Laden:", error)}
console.error("Es gab ein Fehler beim Laden des PDFs:", error)
}
onSourceError={(error) => console.error("Ungültige PDF:", error)} onSourceError={(error) => console.error("Ungültige PDF:", error)}
> >
{containerWidth && ( {containerWidth && (
<Page pageNumber={pageNumber} width={containerWidth * 0.8} /> <Page
pageNumber={pageNumber}
width={containerWidth * 0.8}
customTextRenderer={textRenderer}
/>
)} )}
</Document> </Document>
</Box> </Box>
<Box <Box mt={2} display="flex" alignItems="center" justifyContent="center" gap={1}>
mt={2} <IconButton disabled={pageNumber <= 1} onClick={() => setPageNumber((p) => p - 1)}>
display="flex"
alignItems="center"
justifyContent="center"
gap={1}
>
<IconButton
disabled={pageNumber <= 1}
onClick={() => setPageNumber((p) => p - 1)}
>
<ArrowCircleLeftIcon fontSize="large" /> <ArrowCircleLeftIcon fontSize="large" />
</IconButton> </IconButton>
<span> <span>{pageNumber} / {numPages}</span>
{pageNumber} / {numPages} <IconButton disabled={pageNumber >= (numPages || 1)} onClick={() => setPageNumber((p) => p + 1)}>
</span>
<IconButton
disabled={pageNumber >= (numPages || 1)}
onClick={() => setPageNumber((p) => p + 1)}
>
<ArrowCircleRightIcon fontSize="large" /> <ArrowCircleRightIcon fontSize="large" />
</IconButton> </IconButton>
</Box> </Box>