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

@ -1,145 +1,130 @@
import { import {
Table, TableBody, TableCell, TableContainer, Table, TableBody, TableCell, TableContainer,
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 { label: 'Fondsname', value: 'Fund Real Estate Prime Europe', page: 1, status: 'ok' },
const exampleData = [ { label: 'Fondsmanager', value: '', page: 1, status: 'error' },
{ label: 'Fondsname', value: 'Fund Real Estate Prime Europe', page: 1, status: 'ok' }, { label: 'Risikoprofil', value: 'Core/Core+', page: 10, status: 'warning' },
{ label: 'Fondsmanager', value: '', page: 1, status: 'error' }, { label: 'LTV', value: '30-35 %', page: 8, status: 'ok' },
{ label: 'Risikoprofil', value: 'Core/Core+', page: 10, status: 'warning' }, { label: 'Ausschüttungsrendite', value: '4%', page: 34, status: 'ok' }
{ label: 'LTV', value: '30-35 %', page: 8, status: 'ok' }, ];
{ label: 'Ausschüttungsrendite', value: '4%', page: 34, status: 'ok' }
]; interface KennzahlenTableProps {
onPageClick?: (page: number) => void;
interface KennzahlenTableProps { setSelectedLabel?: (label: string) => void;
onPageClick?: (page: number) => void; }
}
export default function KennzahlenTable({ onPageClick, setSelectedLabel }: KennzahlenTableProps) {
// React-Komponente const [rows, setRows] = useState(exampleData);
export default function KennzahlenTable({ onPageClick }: KennzahlenTableProps) { const [open, setOpen] = useState(false);
// Zustand für bearbeitbare Daten const [currentValue, setCurrentValue] = useState('');
const [rows, setRows] = useState(exampleData); const [currentIndex, setCurrentIndex] = useState<number | null>(null);
// Zustände für Dialog-Funktion const handleEditClick = (value: string, index: number) => {
const [open, setOpen] = useState(false); // Dialog anzeigen? setCurrentValue(value);
const [currentValue, setCurrentValue] = useState(''); // Eingabewert setCurrentIndex(index);
const [currentIndex, setCurrentIndex] = useState<number | null>(null); // Zeilenindex setOpen(true);
};
// Beim Klick auf das Stift-Icon: Dialog öffnen
const handleEditClick = (value: string, index: number) => { const handleSave = () => {
setCurrentValue(value); if (currentIndex !== null) {
setCurrentIndex(index); const updated = [...rows];
setOpen(true); updated[currentIndex].value = currentValue;
}; setRows(updated);
}
// Wert speichern und Dialog schließen setOpen(false);
const handleSave = () => { };
if (currentIndex !== null) {
const updated = [...rows]; return (
updated[currentIndex].value = currentValue; <>
setRows(updated); <TableContainer component={Paper}>
} <Table>
setOpen(false); <TableHead>
}; <TableRow>
<TableCell><strong>Kennzahl</strong></TableCell>
return ( <TableCell><strong>Wert</strong></TableCell>
<> <TableCell><strong>Seite</strong></TableCell>
<TableContainer component={Paper}> </TableRow>
<Table> </TableHead>
{/* Tabellenkopf */} <TableBody>
<TableHead> {rows.map((row, index) => {
<TableRow> let borderColor = 'transparent';
<TableCell><strong>Kennzahl</strong></TableCell> if (row.status === 'error') borderColor = 'red';
<TableCell><strong>Wert</strong></TableCell> else if (row.status === 'warning') borderColor = '#f6ed48';
<TableCell><strong>Seite</strong></TableCell>
</TableRow> return (
</TableHead> <TableRow
key={index}
{/* Tabelleninhalt */} onClick={() => setSelectedLabel?.(row.label)}
<TableBody> hover
{rows.map((row, index) => { sx={{ cursor: 'pointer' }}
// Rahmenfarbe anhand Status >
let borderColor = 'transparent'; <TableCell>{row.label}</TableCell>
if (row.status === 'error') borderColor = 'red'; <TableCell>
else if (row.status === 'warning') borderColor = '#f6ed48'; <Box
sx={{
return ( border: `2px solid ${borderColor}`,
<TableRow key={index}> borderRadius: 1,
{/* Kennzahl */} padding: '4px 8px',
<TableCell>{row.label}</TableCell> display: 'flex',
alignItems: 'center',
{/* Wert mit Status-Icons + Stift rechts */} justifyContent: 'space-between',
<TableCell> width: '100%',
<Box }}
sx={{ >
border: `2px solid ${borderColor}`, <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
borderRadius: 1, {row.status === 'error' && <ErrorOutlineIcon fontSize="small" color="error" />}
padding: '4px 8px', {row.status === 'warning' && <SearchIcon fontSize="small" sx={{ color: '#f6ed48' }} />}
display: 'flex', <span>{row.value || '—'}</span>
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{row.status === 'error' && <ErrorOutlineIcon fontSize="small" color="error" />}
{row.status === 'warning' && <SearchIcon fontSize="small" sx={{ color: '#f6ed48' }} />}
<span>{row.value || '—'}</span>
</Box>
{/* Stift-Icon */}
<EditIcon
fontSize="small"
sx={{ color: '#555', cursor: 'pointer' }}
onClick={() => handleEditClick(row.value, index)}
/>
</Box> </Box>
</TableCell> <EditIcon
fontSize="small"
{/* Seitenzahl */} sx={{ color: '#555', cursor: 'pointer' }}
<TableCell> onClick={() => handleEditClick(row.value, index)}
<Link />
component="button" </Box>
onClick={() => onPageClick?.(row.page)} </TableCell>
sx={{ cursor: 'pointer' }} <TableCell>
> <Link
{row.page} component="button"
</Link> onClick={() => onPageClick?.(row.page)}
</TableCell> sx={{ cursor: 'pointer' }}
</TableRow> >
); {row.page}
})} </Link>
</TableBody> </TableCell>
</Table> </TableRow>
</TableContainer> );
})}
{/* Dialog zum Bearbeiten */} </TableBody>
<Dialog open={open} onClose={() => setOpen(false)}> </Table>
<DialogTitle>Kennzahl bearbeiten</DialogTitle> </TableContainer>
<DialogContent>
<TextField <Dialog open={open} onClose={() => setOpen(false)}>
fullWidth <DialogTitle>Kennzahl bearbeiten</DialogTitle>
value={currentValue} <DialogContent>
onChange={(e) => setCurrentValue(e.target.value)} <TextField
label="Neuer Wert" fullWidth
variant="outlined" value={currentValue}
margin="dense" onChange={(e) => setCurrentValue(e.target.value)}
/> label="Neuer Wert"
</DialogContent> variant="outlined"
<DialogActions> margin="dense"
<Button onClick={() => setOpen(false)}>Abbrechen</Button> />
<Button onClick={handleSave} variant="contained">Speichern</Button> </DialogContent>
</DialogActions> <DialogActions>
</Dialog> <Button onClick={() => setOpen(false)}>Abbrechen</Button>
</> <Button onClick={handleSave} variant="contained">Speichern</Button>
); </DialogActions>
} </Dialog>
</>
);
}

View File

@ -1,118 +1,124 @@
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;
} }
export default function PDFViewer({ pitchBookId, currentPage }: PDFViewerProps) { 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 [pdfKey, setPdfKey] = useState(Date.now()); const [highlightLabels, setHighlightLabels] = useState<string[]>([]);
const containerRef = useRef<HTMLDivElement>(null); const [pdfKey, setPdfKey] = useState(Date.now());
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => { const containerRef = useRef<HTMLDivElement>(null);
setNumPages(numPages);
};
useEffect(() => { const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
const updateWidth = () => { setNumPages(numPages);
if (containerRef.current) { };
setContainerWidth(containerRef.current.offsetWidth);
}
};
updateWidth(); // Resize
window.addEventListener("resize", updateWidth); useEffect(() => {
return () => window.removeEventListener("resize", updateWidth); const updateWidth = () => {
}, []); if (containerRef.current) {
setContainerWidth(containerRef.current.offsetWidth);
}
};
updateWidth();
window.addEventListener("resize", updateWidth);
return () => window.removeEventListener("resize", updateWidth);
}, []);
useEffect(() => { // Highlight-Wörter setzen
if (currentPage && currentPage !== pageNumber) { useEffect(() => {
setPageNumber(currentPage); setHighlightLabels(["LTV", "Fondsmanager", "Risikoprofil"]);
} }, []);
}, [currentPage]);
useEffect(() => { // Highlight-Logik
const handleProgress = (data: { id: number; progress: number }) => { const highlightPattern = (text: string, patterns: string[]) => {
if (data.id.toString() === pitchBookId && data.progress === 50) { for (const word of patterns) {
setPdfKey(Date.now()); const regex = new RegExp(`(${word})`, "gi");
} text = text.replace(regex, "<mark>$1</mark>");
}; }
return text;
};
socket.on("progress", handleProgress); const textRenderer = useCallback(
(textItem: { str: string }) => highlightPattern(textItem.str, highlightLabels),
[highlightLabels]
);
return () => { // Seitenwechsel bei Prop-Änderung
socket.off("progress", handleProgress); useEffect(() => {
}; if (currentPage && currentPage !== pageNumber) {
}, [pitchBookId]); setPageNumber(currentPage);
}
}, [currentPage]);
return ( // Socket: Re-Render bei Fortschritt
<Box useEffect(() => {
display="flex" const handleProgress = (data: { id: number; progress: number }) => {
flexDirection="column" if (data.id.toString() === pitchBookId && data.progress === 50) {
justifyContent="center" setPdfKey(Date.now()); // Force re-render
alignItems="center" }
width="100%" };
height="100%" socket.on("progress", handleProgress);
p={2} return () => {
> socket.off("progress", handleProgress);
<Box };
ref={containerRef} }, [pitchBookId]);
sx={{
width: "100%", return (
maxHeight: "90vh", <Box display="flex" flexDirection="column" justifyContent="center" alignItems="center" width="100%" height="100%" p={2}>
overflow: "auto", <Box
display: "flex", ref={containerRef}
justifyContent: "center", sx={{
alignItems: "center", width: "100%",
}} maxHeight: "90vh",
> overflow: "auto",
<Document display: "flex",
key={pdfKey} justifyContent: "center",
file={`http://localhost:5050/api/pitch_book/${pitchBookId}/download`} alignItems: "center",
onLoadSuccess={onDocumentLoadSuccess} }}
onLoadError={(error) => >
console.error("Es gab ein Fehler beim Laden des PDFs:", error) <Document
} key={pdfKey}
onSourceError={(error) => console.error("Ungültige PDF:", error)} file={`http://localhost:5050/api/pitch_book/${pitchBookId}/download`}
> onLoadSuccess={onDocumentLoadSuccess}
{containerWidth && ( onLoadError={(error) => console.error("Fehler beim Laden:", error)}
<Page pageNumber={pageNumber} width={containerWidth * 0.8} /> onSourceError={(error) => console.error("Ungültige PDF:", error)}
)} >
</Document> {containerWidth && (
</Box> <Page
<Box pageNumber={pageNumber}
mt={2} width={containerWidth * 0.8}
display="flex" customTextRenderer={textRenderer}
alignItems="center" />
justifyContent="center" )}
gap={1} </Document>
> </Box>
<IconButton <Box mt={2} display="flex" alignItems="center" justifyContent="center" gap={1}>
disabled={pageNumber <= 1} <IconButton disabled={pageNumber <= 1} onClick={() => setPageNumber((p) => p - 1)}>
onClick={() => setPageNumber((p) => p - 1)} <ArrowCircleLeftIcon fontSize="large" />
> </IconButton>
<ArrowCircleLeftIcon fontSize="large" /> <span>{pageNumber} / {numPages}</span>
</IconButton> <IconButton disabled={pageNumber >= (numPages || 1)} onClick={() => setPageNumber((p) => p + 1)}>
<span> <ArrowCircleRightIcon fontSize="large" />
{pageNumber} / {numPages} </IconButton>
</span> </Box>
<IconButton </Box>
disabled={pageNumber >= (numPages || 1)} );
onClick={() => setPageNumber((p) => p + 1)} }
>
<ArrowCircleRightIcon fontSize="large" />
</IconButton>
</Box>
</Box>
);
}