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

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;
} }
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"]);
} }, []);

das auch...

das auch...
}, [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>
);
}