frontend/31-highlight-kennzahlen #47
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue