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";

Das musste vom mergen sein. Das muss überall weg. Sonst kann ich es nicht testen...

Das musste vom mergen sein. Das muss überall weg. Sonst kann ich es nicht testen...
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"]);
}, []);

das auch...

das auch...
// 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>