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