pse2_ff/project/frontend/src/routes/extractedResult_.$pitchBook...

561 lines
15 KiB
TypeScript

import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import EditIcon from "@mui/icons-material/Edit";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
IconButton,
Link,
Paper,
Radio,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Typography,
} from "@mui/material";
import {
useMutation,
useQueryClient,
useSuspenseQuery,
} from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useState, type KeyboardEvent } from "react";
import PDFViewer from "../components/pdfViewer";
import { fetchPutKPI } from "../util/api";
import { kpiQueryOptions } from "../util/query";
import { redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/extractedResult_/$pitchBook/$kpi")({
component: ExtractedResultsPage,
validateSearch: (search: Record<string, unknown>) => {
return {
from: typeof search.from === "string" ? search.from : undefined,
};
},
loader: async ({ context: { queryClient }, params: { pitchBook } }) => {
try {
return await queryClient.ensureQueryData(kpiQueryOptions(pitchBook));
} catch (err) {
throw redirect({
to: "/"
});
}
},
});
function ExtractedResultsPage() {
const params = Route.useParams() as { pitchBook: string; kpi: string };
const { pitchBook, kpi } = params;
const navigate = useNavigate();
const queryClient = useQueryClient();
const { from } = Route.useSearch();
const { data: kpiData } = useSuspenseQuery(kpiQueryOptions(pitchBook));
const kpiValues = kpiData[kpi.toUpperCase()] || [];
const [selectedIndex, setSelectedIndex] = useState(0);
const [currentPage, setCurrentPage] = useState(kpiValues[0]?.page || 1);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [customValue, setCustomValue] = useState("");
const [customPage, setCustomPage] = useState("");
const [editingCustomPage, setEditingCustomPage] = useState(false);
const [focusHighlightOverride, setFocusHighlightOverride] = useState<{ page: number; text: string } | null>(null);
const originalValue = kpiValues[0]?.entity || "";
const originalPage = kpiValues[0]?.page || 0;
// Funktion, um gleiche Werte zusammenzufassen und die Seiten zu sammeln
function groupKpiValues(values: Array<{ entity: string; page: number; [key: string]: any }>): Array<{ entity: string; pages: number[]; [key: string]: any }> {
const map = new Map<string, { entity: string; pages: number[]; [key: string]: any }>();
values.forEach((item: { entity: string; page: number; [key: string]: any }) => {
const key = item.entity.toLowerCase();
if (!map.has(key)) {
map.set(key, { ...item, pages: [item.page] });
} else {
const existingEntry = map.get(key)!;
if (!existingEntry.pages.includes(item.page)) {
existingEntry.pages.push(item.page);
}
}
});
return Array.from(map.values());
}
const groupedKpiValues: Array<{ entity: string; pages: number[]; [key: string]: any }> = groupKpiValues(kpiValues);
const selectedValue: string =
selectedIndex === -1 ? customValue : groupedKpiValues[selectedIndex]?.entity || "";
const selectedPage =
selectedIndex === -1
? (parseInt(customPage) > 0 ? parseInt(customPage) : 1)
: groupedKpiValues[selectedIndex]?.pages[0] || 1;
// Um zu prüfen, ob der Wert nur aus Leerzeichen besteht
const isSelectedValueEmpty = selectedIndex === -1 ? customValue.trim() === "" : !selectedValue;
const focusHighlight = focusHighlightOverride || {
page: groupedKpiValues.at(selectedIndex)?.pages[0] || -1,
text: groupedKpiValues.at(selectedIndex)?.entity || "",
};
useEffect(() => {
const valueChanged = selectedValue !== originalValue;
const pageChanged = selectedPage !== originalPage;
setHasChanges(valueChanged || pageChanged);
}, [selectedValue, selectedPage, originalValue, originalPage]);
const { mutate: updateKPI } = useMutation({
mutationFn: () => {
const updatedData = { ...kpiData };
let baseObject;
if (selectedIndex >= 0) {
// Das Originalobjekt mit allen Feldern für diesen Wert suchen
const original = kpiValues.find(v => v.entity.toLowerCase() === groupedKpiValues[selectedIndex].entity.toLowerCase()) as { status?: string; source?: string } | undefined;
baseObject = {
label: kpi.toUpperCase(),
entity: groupedKpiValues[selectedIndex].entity,
page: groupedKpiValues[selectedIndex].pages[0],
status: original?.status || "single-source",
source: original?.source || "auto",
};
} else {
baseObject = {
label: kpi.toUpperCase(),
entity: selectedValue,
page: selectedPage,
status: "single-source",
source: "manual",
};
}
updatedData[kpi.toUpperCase()] = [
{
...baseObject,
entity: selectedValue,
page: selectedPage,
},
];
return fetchPutKPI(Number(pitchBook), updatedData);
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["pitchBookKPI", pitchBook],
});
navigate({
to: "/extractedResult/$pitchBook",
params: { pitchBook },
search: from ? { from } : undefined
});
},
onError: (error) => {
console.error("Error updating KPI:", error);
},
});
const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
if (value === "custom") {
setSelectedIndex(-1);
setFocusHighlightOverride(null);
} else {
const index = Number.parseInt(value);
setSelectedIndex(index);
setCurrentPage(groupedKpiValues[index].pages[0]);
setCustomValue("");
setCustomPage("");
setFocusHighlightOverride(null);
}
};
const handleCustomValueChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const value = event.target.value;
setCustomValue(value);
setSelectedIndex(-1);
setFocusHighlightOverride(null);
}
const handleCustomPageChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const value = event.target.value;
// Allow empty string or positive numbers only (no 0)
if (value === '' || (/^\d+$/.test(value) && parseInt(value) > 0)) {
setCustomPage(value);
}
};
const handleRowClick = (index: number) => {
setCurrentPage(groupedKpiValues[index].pages[0]);
setSelectedIndex(index);
setCustomValue("");
setCustomPage("");
setFocusHighlightOverride(null);
};
const handlePageClick = (page: number, entity: string) => {
setCurrentPage(page);
setFocusHighlightOverride({
page: page,
text: entity,
});
};
const handleBackClick = () => {
if (hasChanges) {
setShowConfirmDialog(true);
} else {
navigate({
to: "/extractedResult/$pitchBook",
params: { pitchBook },
search: from ? { from } : undefined
});
}
};
const handleConfirmDiscard = () => {
setShowConfirmDialog(false);
navigate({
to: "/extractedResult/$pitchBook",
params: { pitchBook },
search: from ? { from } : undefined
});
};
const handleCancelDiscard = () => {
setShowConfirmDialog(false);
};
const handleAcceptReview = () => {
updateKPI();
};
const startCustomPageEditing = () => {
setEditingCustomPage(true);
};
const handleCustomPageKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" || e.key === "Escape") {
setEditingCustomPage(false);
}
};
return (
<Box p={4}>
<Box sx={{ display: "flex", alignItems: "center", mb: 3 }}>
<IconButton onClick={handleBackClick} sx={{ mr: 2 }}>
<ArrowBackIcon fontSize="large" sx={{ color: "#383838" }} />
</IconButton>
<Typography variant="h5">
Überprüfung der Kennzahl: <b><u>{kpi}</u></b>
</Typography>
</Box>
<Box
display="flex"
gap={4}
sx={{
width: "100vw",
maxWidth: "100%",
height: "85vh",
mt: 4,
}}
>
<Paper
elevation={2}
sx={{
width: "45%",
maxHeight: "100%",
height: "fit-content",
borderRadius: 2,
backgroundColor: "#eeeeee",
padding: 2,
overflow: "auto",
}}
>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell width="85%">
<strong>Gefundene Werte</strong>
</TableCell>
<TableCell align="center" width="15%">
<strong>Seiten</strong>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{groupedKpiValues.map((item, index) => (
<TableRow
key={`${item.entity}_${item.pages.join('_')}_${index}`}
sx={{
"&:hover": { backgroundColor: "#f9f9f9" },
cursor: "pointer",
}}
onClick={() => handleRowClick(index)}
>
<TableCell>
<Box
sx={{
borderRadius: 1,
padding: "4px 8px",
display: "flex",
alignItems: "center",
width: "100%",
cursor: "pointer",
"&:hover": {
borderColor: "#ccc",
},
}}
>
<Radio
value={index.toString()}
checked={selectedIndex === index}
onChange={handleRadioChange}
sx={{
color: "#383838",
"&.Mui-checked": { color: "#383838" },
padding: "4px",
marginRight: 1,
"&:focus": {
outline: "none",
},
}}
/>
<span>{item.entity}</span>
</Box>
</TableCell>
<TableCell align="center">
{item.pages.map((page: number, i: number) => (
<Link
key={page}
component="button"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
handlePageClick(page, item.entity);
}}
sx={{ cursor: "pointer", ml: i > 0 ? 1 : 0 }}
>
{page}
</Link>
))}
</TableCell>
</TableRow>
))}
<TableRow>
<TableCell>
<Box
sx={{
padding: "4px 8px",
display: "flex",
alignItems: "center",
width: "100%",
cursor: "pointer",
"&:hover": {
borderColor: "#ccc",
},
}}
onClick={() => {
setSelectedIndex(-1);
setFocusHighlightOverride(null);
}}
>
<Radio
value="custom"
checked={selectedIndex === -1 && customValue !== ""}
onChange={handleRadioChange}
sx={{
color: "#383838",
"&.Mui-checked": { color: "#383838" },
padding: "4px",
marginRight: 1,
"&:focus": {
outline: "none",
},
}}
/>
<Box sx={{ width: '100%' }}>
<TextField
placeholder="Einen abweichenden Wert eingeben..."
value={customValue}
onChange={handleCustomValueChange}
variant="standard"
fullWidth
InputProps={{
disableUnderline: true,
}}
sx={{
"& .MuiInput-input": {
padding: 0,
},
}}
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
}}
error={selectedIndex === -1 && customValue !== "" && customValue.trim() === ""}
helperText={selectedIndex === -1 && customValue !== "" && customValue.trim() === "" ? "Der Wert, der angegeben wurde, ist leer." : ""}
/>
</Box>
</Box>
</TableCell>
<TableCell align="center">
{editingCustomPage ? (
<TextField
value={customPage}
onChange={handleCustomPageChange}
onKeyDown={handleCustomPageKeyPress}
onBlur={() => setEditingCustomPage(false)}
autoFocus
size="small"
variant="standard"
sx={{
width: "60px",
"& .MuiInput-input": {
textAlign: "center"
}
}}
inputProps={{
min: 0,
style: { textAlign: 'center' }
}}
/>
) : (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 1,
cursor: "pointer",
minHeight: "24px",
minWidth: "100px",
margin: "0 auto",
}}
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
startCustomPageEditing();
}}
>
<span>{customPage || "..."}</span>
<EditIcon
fontSize="small"
sx={{
color: "black",
opacity: 0.7,
transition: "opacity 0.2s ease",
ml: 1
}}
onClick={(e) => {
e.stopPropagation();
startCustomPageEditing();
}}
/>
</Box>
)}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Paper>
<Box
display="flex"
flexDirection="column"
justifyContent="space-between"
gap={3}
sx={{ width: "55%", height: "95%" }}
>
<Paper
elevation={2}
sx={{
width: "100%",
height: "fit-content",
maxHeight: "100%",
borderRadius: 2,
backgroundColor: "#eeeeee",
display: "flex",
flexDirection: "column",
overflow: "auto",
padding: 2,
}}
>
<PDFViewer
pitchBookId={pitchBook}
currentPage={currentPage}
onPageChange={setCurrentPage}
highlight={groupedKpiValues
.map((k) => k.pages.map((page: number) => ({ page, text: k.entity })))
.reduce((acc, val) => acc.concat(val), [])}
focusHighlight={focusHighlight}
/>
</Paper>
<Box mt={2} display="flex" justifyContent="flex-end" gap={2}>
<Button
variant="contained"
onClick={handleAcceptReview}
disabled={isSelectedValueEmpty}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
"&.Mui-disabled": { backgroundColor: "#ccc" },
}}
>
Überprüfung Annehmen
</Button>
</Box>
</Box>
</Box>
<Dialog
open={showConfirmDialog}
onClose={handleCancelDiscard}
maxWidth="sm"
fullWidth
>
<DialogTitle sx={{ fontSize: "1.25rem", fontWeight: "bold" }}>
Achtung
</DialogTitle>
<DialogContent>
<DialogContentText sx={{ fontSize: "1rem" }}>
Alle vorgenommenen Änderungen werden verworfen.
</DialogContentText>
</DialogContent>
<DialogActions sx={{ p: 3, gap: 2 }}>
<Button
onClick={handleCancelDiscard}
variant="outlined"
sx={{
color: "#666",
borderColor: "#ddd",
"&:hover": { backgroundColor: "#f5f5f5" },
}}
>
Abbrechen
</Button>
<Button
onClick={handleConfirmDiscard}
variant="contained"
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
>
Bestätigen
</Button>
</DialogActions>
</Dialog>
</Box>
);
}