pse2_ff/project/frontend/src/components/KennzahlenTable.tsx

476 lines
13 KiB
TypeScript

import type { Kennzahl } from "@/types/kpi";
import EditIcon from "@mui/icons-material/Edit";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import SearchIcon from "@mui/icons-material/Search";
import {
Box,
Link,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Tooltip,
} from "@mui/material";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import type { KeyboardEvent } from "react";
import { fetchPutKPI } from "../util/api";
interface KennzahlenTableProps {
onPageClick?: (page: number, text: string) => void;
pdfId: string;
settings: Kennzahl[];
data: {
[key: string]: {
label: string;
entity: string;
page: number;
status: string;
source: string;
}[];
};
from?: string;
}
export default function KennzahlenTable({
onPageClick,
data,
pdfId,
settings,
from,
}: KennzahlenTableProps) {
const [editingIndex, setEditingIndex] = useState<string>("");
const [editValue, setEditValue] = useState("");
const [editingPageIndex, setEditingPageIndex] = useState<string>("");
const [editPageValue, setEditPageValue] = useState("");
const [hoveredPageIndex, setHoveredPageIndex] = useState<string>("");
const navigate = useNavigate();
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: (params: { id: string; newValue?: string; newPage?: number }) => {
const { id, newValue, newPage } = params;
const key = id.toUpperCase();
const updatedData = { ...data };
if (data[key] && data[key].length > 0) {
updatedData[key] = data[key].map((item) => ({
...item,
...(newValue !== undefined && { entity: newValue }),
...(newPage !== undefined && { page: newPage }),
}));
} else {
updatedData[key] = [{
label: key,
entity: newValue || "",
page: newPage || 0,
status: "single-source",
source: "manual"
}];
}
return fetchPutKPI(Number(pdfId), updatedData);
},
onMutate: async (params: { id: string; newValue?: string; newPage?: number }) => {
const { id, newValue, newPage } = params;
await queryClient.cancelQueries({
queryKey: ["pitchBookKPI", pdfId],
});
const snapshot = queryClient.getQueryData(["pitchBookKPI", pdfId]);
const key = id.toUpperCase();
queryClient.setQueryData(["pitchBookKPI", pdfId], () => {
const updatedData = { ...data };
if (data[key] && data[key].length > 0) {
updatedData[key] = data[key].map((item) => ({
...item,
...(newValue !== undefined && { entity: newValue }),
...(newPage !== undefined && { page: newPage }),
}));
} else {
updatedData[key] = [{
label: key,
entity: newValue || "",
page: newPage || 0,
status: "single-source",
source: "manual"
}];
}
return updatedData;
});
return () => {
queryClient.setQueryData(["pitchBookKPI", pdfId], snapshot);
};
},
onError: (error, _variables, rollback) => {
console.log("error", error);
rollback?.();
},
onSettled: () => {
return queryClient.invalidateQueries({
queryKey: ["pitchBookKPI", pdfId],
});
},
});
// Bearbeitung starten
const startEditing = (value: string, index: string) => {
setEditingIndex(index);
setEditValue(value);
};
const startPageEditing = (value: number, index: string) => {
setEditingPageIndex(index);
setEditPageValue(value.toString());
};
// Bearbeitung beenden und Wert speichern
const handleSave = async (index: string) => {
mutate({ id: index, newValue: editValue });
setEditingIndex("");
};
const handlePageSave = async (index: string) => {
const pageNumber = parseInt(editPageValue);
if (editPageValue === "" || pageNumber === 0) {
mutate({ id: index, newPage: 0 });
} else if (!isNaN(pageNumber) && pageNumber > 0) {
mutate({ id: index, newPage: pageNumber });
}
setEditingPageIndex("");
};
// Tastatureingaben verarbeiten
const handleKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: string) => {
if (e.key === "Enter") {
handleSave(index);
} else if (e.key === "Escape") {
setEditingIndex("");
}
};
const handlePageKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: string) => {
if (e.key === "Enter") {
handlePageSave(index);
} else if (e.key === "Escape") {
setEditingPageIndex("");
}
};
const handleNavigateToDetail = (settingName: string) => {
navigate({
to: "/extractedResult/$pitchBook/$kpi",
params: {
pitchBook: pdfId,
kpi: settingName,
},
search: { from: from ?? undefined },
});
};
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell width="30%">
<strong>Kennzahl</strong>
</TableCell>
<TableCell width="55%">
<strong>Wert</strong>
</TableCell>
<TableCell align="center" width="15%">
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 1 }}>
<strong>Seite</strong>
</Box>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{settings
.filter((setting) => setting.active)
.sort((a, b) => a.position - b.position)
.map((setting) => ({
setting: setting,
extractedValues: data[setting.name.toUpperCase()] || [],
}))
.map((row) => {
let borderColor = "transparent";
const hasMultipleValues = row.extractedValues.length > 1;
const hasNoValue =
row.setting.mandatory &&
(row.extractedValues.length === 0 ||
row.extractedValues.at(0)?.entity === "");
if (hasNoValue) {
borderColor = "red";
} else if (hasMultipleValues) {
borderColor = "#f6ed48";
}
const currentPage = row.extractedValues.at(0)?.page ?? 0;
const isPageHovered = hoveredPageIndex === row.setting.name;
const canEditPage = !hasMultipleValues;
return (
<TableRow key={row.setting.name}>
<TableCell>{row.setting.name}
{row.setting.mandatory && (
<span> *</span>
)}
</TableCell>
<TableCell
onClick={() => {
// Only allow inline editing for non-multiple value cells
if (!hasMultipleValues) {
startEditing(
row.extractedValues.at(0)?.entity || "",
row.setting.name,
);
} else {
// Navigate to detail page for multiple values
handleNavigateToDetail(row.setting.name);
}
}}
>
{hasMultipleValues ? (
<Tooltip
title={
<>
<b>Problem</b>
<br />
Mehrere Werte für die Kennzahl gefunden.
</>
}
placement="bottom"
arrow
>
<Box
sx={{
border: `2px solid ${borderColor}`,
borderRadius: 1,
padding: "4px 8px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
cursor: "pointer",
"&:hover": {
backgroundColor: "#f5f5f5",
},
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
width: "100%",
}}
>
<span>
{row.extractedValues.at(0)?.entity || "—"}
</span>
</Box>
<SearchIcon
fontSize="small"
sx={{ color: "#f6ed48" }}
/>
</Box>
</Tooltip>
) : (
<Tooltip
title={
hasNoValue ? (
<>
<b>Problem</b>
<br />
Es wurden keine Kennzahlen gefunden. Bitte
ergänzen!
</>
) : (
""
)
}
placement="bottom"
arrow
>
<span>
<Box
sx={{
border: `2px solid ${borderColor}`,
borderRadius: 1,
padding: "4px 8px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
cursor: "text",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
width: "100%",
}}
>
{hasNoValue && (
<ErrorOutlineIcon
fontSize="small"
color="error"
/>
)}
{editingIndex === row.setting.name ? (
<TextField
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={(e) =>
handleKeyPress(e, row.setting.name)
}
onBlur={() => handleSave(row.setting.name)}
autoFocus
size="small"
fullWidth
variant="standard"
sx={{ margin: "-8px 0" }}
/>
) : (
<span>
{row.extractedValues.at(0)?.entity || "—"}
</span>
)}
</Box>
<EditIcon
fontSize="small"
sx={{ color: "#555", cursor: "pointer" }}
onClick={(e) => {
e.stopPropagation();
startEditing(
row.extractedValues.at(0)?.entity || "",
row.setting.name,
);
}}
/>
</Box>
</span>
</Tooltip>
)}
</TableCell>
<TableCell align="center">
{editingPageIndex === row.setting.name ? (
<TextField
value={editPageValue}
onChange={(e) => {
const value = e.target.value;
if (value === '' || /^\d+$/.test(value)) {
setEditPageValue(value);
}
}}
onKeyDown={(e) => handlePageKeyPress(e, row.setting.name)}
onBlur={() => handlePageSave(row.setting.name)}
autoFocus
size="small"
variant="standard"
sx={{
width: "60px",
"& .MuiInput-input": {
textAlign: "center"
}
}}
inputProps={{
min: 0,
style: { textAlign: 'center' }
}}
/>
) : (
<>
{currentPage > 0 ? (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
cursor: "pointer",
borderRadius: "4px",
minHeight: "32px",
minWidth: "100px",
}}
onClick={() => {
if (canEditPage) {
startPageEditing(currentPage, row.setting.name);
}
}}
>
<Link
component="button"
onClick={(e) => {
e.stopPropagation();
const extractedValue = row.extractedValues.at(0);
if (extractedValue?.page && extractedValue.page > 0) {
onPageClick?.(Number(extractedValue.page), extractedValue.entity || "");
}
}}
sx={{ cursor: "pointer" }}
>
{currentPage}
</Link>
</Box>
) : canEditPage ? (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
cursor: "pointer",
minHeight: "32px",
minWidth: "100px",
borderRadius: "4px",
backgroundColor: isPageHovered ? "#f8f9fa" : "transparent",
}}
onMouseEnter={() => setHoveredPageIndex(row.setting.name)}
onMouseLeave={() => setHoveredPageIndex("")}
onClick={() => startPageEditing(0, row.setting.name)}
>
<span style={{ color: "#999" }}>...</span>
<EditIcon
fontSize="small"
sx={{
position: "absolute",
left: "70px",
color: "black",
cursor: "pointer",
opacity: 0.7,
transition: "opacity 0.2s ease",
}}
/>
</Box>
) : (
""
)}
</>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
);
}